Documentation/API/Cards

Cards Service

API reference for managing payment cards

VERTEX ENGINE

00

Introduction

The Cards API allows you to manage payment cards for your organization. You can request new cards, view card details, activate cards, set PINs, and manage card status and limits.

Key Concepts

  • ID Reuse: The id from "request card" serves as both request_id (during approval) and card_id (after approval)

  • Visibility: list_requests tracks approval status; list_cards shows issued cards

  • Security: Only the assigned cardholder can access card details/operations (via JWT token)

  • Virtual Cards: Created as ACTIVE; no activation needed

1Step 1 — Request Card

  • Call: svc.card.{partner}.request

  • Send: { account_id, user_id, name, type: 1, use_type: 2, features, limits?, extras? }

  • Note: use_type must be 2 (multi-use); single-use cards not available

  • Receive: { id } — save this; it will be the card_id once approved

2Step 2 — Track Approval

  • Call: svc.card.{partner}.list_requests

  • Send: { account_id } (optionally add user_ids to filter)

  • Look for: status: "APPROVED" on your request (same id from Step 1)

3Step 3 — Get Card Details (PAN/CVV)

  • Call: svc.card.{partner}.get_details

  • Send: { account_id, id } where id is from Step 1

  • Receive: { card_number, expiry_date: "MMYY", cvv }

  • Constraints: Caller must be the assigned cardholder; virtual cards are ACTIVE by default

Optional — List Cards (UI/Discovery)

  • Call: svc.card.{partner}.list_cards

  • Send: { entity_id, account_id, user_ids? }

  • Receive: { id: entity_id, cards: [ { id, last_4, status, ... } ] }

  • Behavior: last_4 masked as "****" when INACTIVE; visible when ACTIVE

Optional — Cardholder Actions

  • set_pin (svc.card.{partner}.set_pin): { account_id, id, pin: "1234" } — 4 digits; 30-minute cooldown

  • update_status (svc.card.{partner}.update_status): { account_id, id, status: "ACTIVE" | "FROZEN" } — freeze/unfreeze card

Card Features and Limits

Card Features

Controls which transaction types and features are enabled for a card.

CardFeatures Object(json)
{
  "domestic": true,       // Allow domestic transactions
  "international": false, // Allow international transactions (default: false)
  "e_commerce": true,     // Allow online/e-commerce purchases
  "atm": true,            // Allow ATM withdrawals
  "pos": true,            // Allow point-of-sale transactions
  "contactless": true     // Allow contactless payments
}

Default Values & Behavior

When requesting a card, the following behavior applies:

  • domestic: Always forced to true (ignores request payload)
  • international: Always forced to false (ignores request payload)
  • e_commerce: Honors request payload (default: true if not specified)
  • atm: Honors request payload (default: true if not specified)
  • pos: Honors request payload (default: true if not specified)
  • contactless: Honors request payload (default: true if not specified)

Usage in Endpoints

  • request: ⚠️ Partial (forces domestic and international; honors e_commerce, atm, pos, contactless)
  • edit: ⚠️ Partial (only updates international, e_commerce, atm; ignores domestic, pos, contactless)
  • list_requests, list_cards: ✅ Returns complete features
  • get_details, update_status, activate_card, set_pin: ❌ Not used

Implementation Notes

⚠️ The international toggle is present in the API but full enablement may be staged.

Card Limits

Transaction spending limits that can be configured for a card. All amounts are in minor units (e.g., cents) of the account currency.

CardLimits Object(json)
{
  "transaction_enabled": true,  // Enable per-transaction limit
  "transaction": 100000,        // Maximum per transaction (e.g., $1,000.00 = 100000 cents)
  "daily_enabled": true,        // Enable daily spending limit
  "daily": 100000,              // Maximum daily spend (e.g., $1,000.00)
  "monthly_enabled": true,      // Enable monthly spending limit
  "monthly": 500000,            // Maximum monthly spend (e.g., $5,000.00)
  "yearly_enabled": false,      // Enable yearly spending limit
  "yearly": 0                   // Maximum yearly spend (disabled)
}

Validation Rules

  • At least one limit required: At least one limit amount must be greater than 0
  • Transaction vs Periodic: transaction ≤ max(daily, monthly, yearly)
  • Daily vs Transaction: dailytransaction
  • Monthly vs Daily: monthlydaily
  • Yearly vs Monthly: yearlymonthly

⚠️ Note: Rules 2 and 3 together mean that when both transaction and daily limits are enabled, they must be equal.

Usage in Endpoints

  • request: ✅ Required (full validation applied)
  • edit: ⚠️ Partial (only updates limit amounts; enabled flags ignored; no validation checks)
  • list_requests, list_cards: ✅ Returns complete limits with enabled flags
  • get_details, update_status, activate_card, set_pin: ❌ Not used

⚠️ Important: The validation rules above apply only during card creation (request). The edit endpoint directly overwrites limit amounts without validation checks.

Error Messages

  • "at least one limit must be provided"
  • "single transaction limit cannot be greater than periodic limit"
  • "daily limit cannot be greater than single transaction limit"
  • "monthly limit cannot be greater than daily limit"
  • "yearly limit cannot be greater than monthly limit"

Request Card

Request a new card for a user in your organization.

Endpoint

svc.card.{partner_id}.request

Request

RequestCardRequest(json)
{
  "account_id": "550e8400-e29b-41d4-a716-446655440000",  // Required - account ID (UUID)
  "user_id": "40ec785e-d915-49de-9053-4630042d8182",     // Required - user ID (UUID)
  "name": "John Doe",                                     // Required - name to appear on the card
  "type": 1,                                              // Required - card type: 1=Virtual, 2=Physical
  "use_type": 2,                                          // Required - card use type: 1=Single (rejected), 2=Multi
  "features": {                                           // Required - API honours these booleans (except international, currently ignored)
    "domestic": true,
    "international": false,
    "e_commerce": true,
    "atm": true,
    "pos": true,
    "contactless": true
  },
  "limits": {                                             // Required - include ≥1 non-zero amount; if transaction & daily enabled they must be equal
    "transaction_enabled": true,
    "transaction": 20000,
    "daily_enabled": true,
    "daily": 20000,
    "monthly_enabled": true,
    "monthly": 20000,
    "yearly_enabled": false,
    "yearly": 0
  },
  "extras": {                                             // Accepted but currently ignored (auto_lock stored with no enforcement yet)
    "auto_lock": "2023-12-31T23:59:59Z"
  }
}

Response

RequestCardResponse(json)
{
  "id": "7aa53a6d-5869-c814-5cee-2af0f5d92aa5"  // UUID of the created card request (becomes card_id upon approval)
}

Limit Validation

Limit validation is strict: at least one amount must be non-zero, and whenever multiple limits are enabled the hierarchy must hold (transaction ≥ daily ≥ monthly ≥ yearly).

Authorization Required

  • The returned id becomes the card_id after the request is approved.
  • Virtual cards are auto‑approved. Physical cards may require Owner/Accountant approval.

List Card Requests

List card requests for an account. Members see their own; owners/accountants can see all.

Endpoint

svc.card.{partner_id}.list_requests

Request

ListOrganisationCardRequestsRequest(json)
{
  "account_id": "550e8400-e29b-41d4-a716-446655440000",  // Required - account ID
  "user_ids": ["40ec785e-d915-49de-9053-4630042d8182"]   // Optional - filter by users
}

Response

ListOrganisationCardRequestsResponse(json)
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "requests": [
    {
      "id": "7aa53a6d-5869-c814-5cee-2af0f5d92aa5",
      "name": "John Doe",
      "type": 1,
      "use_type": 2,
      "created_at": "2023-11-15T10:30:45Z",
      "requested_by": "40ec785e-d915-49de-9053-4630042d8182",
      "assigned_to": "40ec785e-d915-49de-9053-4630042d8182",
      "features": {
        "domestic": true,
        "international": false,
        "e_commerce": true,
        "atm": true,
        "pos": true,
        "contactless": true
      },
      "limits": {
        "transaction_enabled": true,
        "transaction": 20000,
        "daily_enabled": true,
        "daily": 20000,
        "monthly_enabled": true,
        "monthly": 20000,
        "yearly_enabled": false,
        "yearly": 0
      },
      "extras": {},
      "status": "PENDING"
    }
  ]
}

Visibility enforced by upstream auth/policy; service returns by account and optional user_ids.

Respond to Card Request

Approve or reject a card request. Typically restricted to Owner/Accountant roles; enforce via access control. Note that virtual cards are auto-approved.

Endpoint

svc.card.{partner_id}.respond_to_request

Request

RespondToCardRequestRequest(json)
{
  "entity_id": "7632a09c-9408-4cf0-b8dd-cada3f089240",  // Required - entity ID
  "account_id": "550e8400-e29b-41d4-a716-446655440000",  // Required - account ID
  "request_id": "7aa53a6d-5869-c814-5cee-2af0f5d92aa5",  // Required - card request ID
  "approved": true                                        // Optional - omit or false to record a rejection
}

Response

RespondToCardRequestResponse(json)
{}

Authorization

Ensure appropriate role checks (Owner/Accountant) are enforced in your integration.

List Cards

List cards for an organization/account. Members see their own; owners/accountants can see all.

Endpoint

svc.card.{partner_id}.list_cards

Request

ListOrganisationCardsRequest(json)
{
  "entity_id": "7632a09c-9408-4cf0-b8dd-cada3f089240",   // Required - entity ID
  "account_id": "550e8400-e29b-41d4-a716-446655440000",  // Required - account ID
  "user_ids": ["40ec785e-d915-49de-9053-4630042d8182"]    // Optional - filter by users
}

Response

ListOrganisationCardsResponse(json)
{
  "id": "7632a09c-9408-4cf0-b8dd-cada3f089240",  // Entity ID (echoes request.entity_id)
  "cards": [
    {
      "id": "7aa53a6d-5869-c814-5cee-2af0f5d92aa5",
      "name": "John Doe",
      "type": 1,
      "use_type": 2,
      "last_4": "1234",  
      "date_created": "2023-11-15T10:30:45Z",
      "user_id": "40ec785e-d915-49de-9053-4630042d8182",
      "org_id": "7632a09c-9408-4cf0-b8dd-cada3f089240",
      "features": {
        "domestic": true,
        "international": false,
        "e_commerce": true,
        "atm": true,
        "pos": true,
        "contactless": true
      },
      "limits": {
        "transaction_enabled": true,
        "transaction": 20000,
        "daily_enabled": true,
        "daily": 20000,
        "monthly_enabled": true,
        "monthly": 20000,
        "yearly_enabled": false,
        "yearly": 0
      },
      "extras": {},                                  // Currently empty in list responses
      "status": "ACTIVE"
    }
  ]
}

Visibility enforced by upstream auth/policy; service filters by request parameters.

Masking Behavior

When a card is INACTIVE, the last_4 may be masked ("****"). Virtual cards are ACTIVE on creation.

Get Card Details

Retrieve sensitive card details such as the full card number, expiry date, and CVV. This endpoint should be used with caution and only when necessary.

Endpoint

svc.card.{partner_id}.get_details

Request

GetCardDetailsRequest(json)
{
  "account_id": "550e8400-e29b-41d4-a716-446655440000",  // Required - account ID
  "id": "7aa53a6d-5869-c814-5cee-2af0f5d92aa5"            // Required - card ID
}

Response

GetCardDetailsResponse(json)
{
  "card_number": "4111111111111234",  // Full card number
  "expiry_date": "1225",              // Card expiry date (MMYY)
  "cvv": "123"                        // Card verification value
}

Security Warning

This endpoint returns sensitive card information. Ensure you handle this data securely and in compliance with PCI DSS requirements.

Edit Card

Partial update of card configuration (name, features, limits, extras). Some flags and numeric limits are persisted.

Endpoint

svc.card.{partner_id}.edit

Request

EditCardRequest(json)
{
  "entity_id": "7632a09c-9408-4cf0-b8dd-cada3f089240", // Required - org ID
  "card_id": "7aa53a6d-5869-c814-5cee-2af0f5d92aa5",    // Required - card ID
  "name": "John Doe",                                   // Optional - new name
  "features": {
    "domestic": true,
    "international": true,                              // Example update
    "e_commerce": true,
    "atm": true,
    "pos": true,
    "contactless": true
  },
  "limits": {
    "transaction_enabled": true,
    "transaction": 20000,
    "daily_enabled": true,
    "daily": 20000,
    "monthly_enabled": true,
    "monthly": 20000,
    "yearly_enabled": false,
    "yearly": 0
  },
  "extras": {
    "auto_lock": "2024-12-31T23:59:59Z"
  }
}

Response

EditCardResponse(json)
{}

Update Card Status

Change a card's status to ACTIVE or FROZEN. Freezing a card prevents transactions.

Endpoint

svc.card.{partner_id}.update_status

Request

UpdateCardStatusRequest(json)
{
  "account_id": "550e8400-e29b-41d4-a716-446655440000",  // Required - account ID
  "id": "7aa53a6d-5869-c814-5cee-2af0f5d92aa5",            // Required - card ID
  "status": "FROZEN"                                       // New status: ACTIVE or FROZEN
}

Response

UpdateCardStatusResponse(json)
{}

Activate Card

Activate a physical card after it has been received by the user. This is required before the card can be used for transactions.

Endpoint

svc.card.{partner_id}.activate_card

Request

ActivateCardRequest(json)
{
  "account_id": "550e8400-e29b-41d4-a716-446655440000",  // Required - account ID
  "id": "7aa53a6d-5869-c814-5cee-2af0f5d92aa5",            // Optional - card ID (can activate by last_4 only)
  "last_4": "1234"                                         // Required - last 4 digits for verification
}

Response

ActivateCardResponse(json)
{}

Set PIN

Set or change the PIN for a card. The PIN must be exactly 4 digits. A cooldown applies between PIN set requests.

Endpoint

svc.card.{partner_id}.set_pin

Request

SetPINRequest(json)
{
  "account_id": "550e8400-e29b-41d4-a716-446655440000",  // Required - account ID
  "id": "7aa53a6d-5869-c814-5cee-2af0f5d92aa5",            // Required - card ID
  "pin": "1234"                                            // New 4-digit PIN
}

Response

SetPINResponse(json)
{}

Security Warning

PINs are encrypted in transit and storage. Handle this data with care in your application.

Card Transaction Authorization

The Card Transaction Authorization API provides real-time transaction approval capabilities, allowing you to intercept and approve/decline card transactions as they occur. This feature enables enhanced security controls and custom authorization logic for your organization's cards.

Real-time Processing

The authorization system uses NATS JetStream for real-time event streaming, ensuring reliable message delivery and transaction processing within strict time constraints.

Stream Configuration

Authorization Request Stream(yaml)
stream: CARD_TX_AUTH
subjects: 
  - card.transaction.auth.>
max_age: 24h
max_msgs_per_subject: 10000
storage: file
retention: limits
discard: old

Transaction Authorization Request

When a card transaction occurs, you will receive an authorization request event.

Subject Pattern

card.transaction.auth.{card_id}.{transaction_id}

Event Structure

Transaction Authorization Request(json)
{
  "transaction_id": "550e8400-e29b-41d4-a716-446655440000",
  "card_id": "7aa53a6d-5869-c814-5cee-2af0f5d92aa5",
  "user_id": "40ec785e-d915-49de-9053-4630042d8182",
  "entity_id": "7632a09c-9408-4cf0-b8dd-cada3f089240",
  "timestamp": "2024-01-15T10:30:45.123Z",
  "expires_at": "2024-01-15T10:30:50.123Z",  // 5 second timeout
  "merchant": {
    "name": "AMAZON.COM",
    "category_code": "5999",  // MCC code
    "category": "Miscellaneous Retail",
    "city": "Seattle",
    "country": "USA",
    "terminal_id": "12345678"
  },
  "transaction": {
    "type": "PURCHASE",  // PURCHASE, ATM_WITHDRAWAL, REFUND, etc.
    "amount": 15000,     // Amount in cents
    "currency": "USD",
    "method": "CHIP",    // CHIP, CONTACTLESS, SWIPE, ONLINE, etc.
    "pos_entry_mode": "CHIP_PIN",
    "international": false,
    "recurring": false
  },
  "authorization": {
    "available_balance": 50000,  // Available balance in cents
    "daily_spent": 10000,        // Amount spent today
    "monthly_spent": 45000,      // Amount spent this month
    "limits": {
      "transaction": 20000,
      "daily": 50000,
      "monthly": 200000
    }
  },
  "metadata": {
    "ip_address": "192.168.1.1",
    "device_id": "mobile_app_123",
    "location": {
      "latitude": 47.6062,
      "longitude": -122.3321
    }
  }
}

Transaction Authorization Response

You must respond to the authorization request within 5 seconds with your decision.

Response Structure

Transaction Authorization Response(json)
{
  "transaction_id": "550e8400-e29b-41d4-a716-446655440000",
  "decision": "APPROVE",  // APPROVE, DECLINE, REVIEW
  "reason_code": "00",    // ISO 8583 response code
  "reason": "Transaction approved",
  "processed_at": "2024-01-15T10:30:46.500Z",
  "metadata": {
    "risk_score": 15,
    "flags": ["trusted_merchant", "regular_pattern"]
  }
}

Implementation Example

Subscribe to transaction authorization requests for your organization:

JavaScript/TypeScript Implementation(javascript)
import { connect, JSONCodec, createInbox } from 'nats.ws';

const jc = JSONCodec();

// Connect to NATS
const nc = await connect({
  servers: ["wss://hermes.sava.africa"],
  authenticator: jwtAuthenticator(jwt, seed),
});

// Subscribe to transaction authorization requests for your organization
const sub = nc.subscribe('card.transaction.auth.*.*');

(async () => {
  for await (const msg of sub) {
    try {
      const authRequest = jc.decode(msg.data);
      
      // Apply your custom authorization logic
      const decision = await evaluateTransaction(authRequest);
      
      // Respond within 5 seconds
      const response = {
        transaction_id: authRequest.transaction_id,
        decision: decision.approved ? "APPROVE" : "DECLINE",
        reason_code: decision.approved ? "00" : "51",
        reason: decision.reason,
        processed_at: new Date().toISOString(),
        metadata: decision.metadata
      };
      
      msg.respond(jc.encode(response));
      
    } catch (err) {
      console.error('Failed to process auth request:', err);
      // If you don't respond, the transaction will be processed 
      // according to default rules
    }
  }
})();

Custom Authorization Logic

Example implementation of custom authorization rules:

Custom Authorization Logic Example(javascript)
async function evaluateTransaction(authRequest) {
  // Example: Implement custom rules
  const rules = [
    checkVelocityLimits,
    checkMerchantBlacklist,
    checkGeolocation,
    checkTimeRestrictions,
    checkAmountThresholds
  ];
  
  for (const rule of rules) {
    const result = await rule(authRequest);
    if (!result.passed) {
      return {
        approved: false,
        reason: result.reason,
        metadata: { failed_rule: rule.name }
      };
    }
  }
  
  return {
    approved: true,
    reason: "All checks passed",
    metadata: { risk_score: calculateRiskScore(authRequest) }
  };
}

Response Codes

Standard ISO 8583 response codes for transaction authorization:

CodeDescriptionAction
00ApprovedTransaction proceeds
05Do not honorGeneric decline
14Invalid card numberDecline - card issue
51Insufficient fundsDecline - balance issue
54Expired cardDecline - card expired
61Exceeds withdrawal limitDecline - limit exceeded
65Exceeds withdrawal frequencyDecline - velocity limit
85No reason to declineApprove with conditions
91Issuer unavailableRetry or use default rules

Timeout Handling

Response Timeout: 5 seconds
Default Behavior: If no response is received within the timeout, the transaction is processed according to the card's default rules and limits
Partial Responses: Responses received after timeout are logged but not applied

Rate Limits

  • Maximum 100 authorization requests per second per organization
  • Burst capacity of 500 requests
  • Rate limits are applied at the organization level

Service Level Agreement

  • 99.9% uptime for authorization stream
  • P95 latency < 100ms for message delivery
  • Automatic failover to default rules if system is unavailable

Webhook Alternative (Coming Soon)

For systems that cannot maintain persistent NATS connections, we will offer a webhook-based authorization API:

Webhook Configuration(yaml)
POST https://your-domain.com/card/authorize
Headers:
  X-Nexus-Signature: <hmac-sha256-signature>
  X-Nexus-Timestamp: <unix-timestamp>
Body: <authorization-request-json>