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.

Base Subject

All Cards API endpoints use the base subject:svc.card.*

Access Control

Only the assigned cardholder can retrieve details. Card must not be INACTIVE.

Access Control

Only the assigned cardholder can change card status.

Virtual Card Flow (Getting Started)

Key Concepts
  • ID reuse: the id returned from “request card” is the request_id and becomes the card_id after approval/creation.
  • Visibility: list_requests shows request lifecycle; list_cards shows provisioned cards (id, last_4, status).
  • Security: only the assigned cardholder can get_details/set_pin/update_status/activate (derived from token).
Step 1 — Request Card
  • Call: svc.card.{partner}.request
  • Send: { account_id, user_id, name, type: 1, use_type: 2, features, limits?, extras? }
  • Receive: { id } — save this; it will be the card_id once approved.
Step 2 — Track Approval
  • Call: svc.card.{partner}.list_requests
  • Send: { account_id } (optionally add user_ids)
  • Expect: your item with the same id and status: "APPROVED" once processed.
  • Tip: single-use (use_type=1) is not available; the service rejects it.
Step 3 — Get Card Details (PAN/CVV)
  • Call: svc.card.{partner}.get_details
  • Send: { account_id, id } where id is the value from Step 1
  • Receive: { card_number, expiry_date: "MMYY", cvv }
  • Constraints: caller must be the assigned cardholder; card must not be INACTIVE (virtual cards are ACTIVE).
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 is masked ("****") when status is INACTIVE; visible when ACTIVE.
Optional — Cardholder Actions
  • set_pin: { account_id, id, pin: "1234" } — 4 digits; cooldown applies between requests.
  • update_status: { account_id, id, status: "ACTIVE" | "FROZEN" } — cardholder safety control.
Limits & Features Notes
  • Limits: values are minor units (e.g., cents). Validations apply: at least one limit required; daily ≤ single; monthly ≤ daily; yearly ≤ monthly; single ≤ max(periodic).
  • Features: you can enable domestic/international/e_commerce/atm/pos/contactless on request. Edits currently persist a subset (international, e_commerce, atm) and numeric limits; others may not be applied yet.

Card Features and Limits

Card Features

When requesting or configuring cards, you can specify which features should be enabled:

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

Where used:

  • Send with: request (required), edit (optional)
  • Returned by: list_requests.items[*].features, list_cards.cards[*].features
  • Not in: get_details, update_status, activate_card, set_pin

Notes:

  • International toggle present; full enablement may be staged.
  • Edit currently persists a subset (international, e_commerce, atm).

Card Limits

You can set various spending limits on cards:

CardLimits Object (json)(json)
{
  "transaction_enabled": true, // Enable per-transaction limit
  "transaction": 5000,         // Maximum amount per transaction (in cents)
  "daily_enabled": true,       // Enable daily spending limit
  "daily": 20000,              // Maximum daily spend (in cents)
  "monthly_enabled": true,     // Enable monthly spending limit
  "monthly": 100000,           // Maximum monthly spend (in cents)
  "yearly_enabled": false,     // Enable yearly spending limit
  "yearly": 0                  // Maximum yearly spend (in cents)
}

Where used:

  • Send with: request (optional — at least one amount must be > 0), edit (optional)
  • Returned by: list_requests.items[*].limits, list_cards.cards[*].limits
  • Not in: get_details, update_status, activate_card, set_pin

Notes:

  • Units are minor units (e.g., cents) of the account currency.
  • Validation: single ≤ max(daily, monthly, yearly); daily ≤ single; monthly ≤ daily; yearly ≤ monthly.

Card Extras

Additional card features that can be configured:

CardExtras Object (json)(json)
{
  "auto_lock": "2023-12-31T23:59:59Z"  // Automatically lock the card at this time
}

Where used:

  • Send with: request (optional), edit (optional)
  • Returned by: list_requests.items[*].extras, list_cards.cards[*].extras (currently {} in list_cards)
  • Not in: get_details, update_status, activate_card, set_pin

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 - card features
    "domestic": true,
    "international": false,
    "e_commerce": true,
    "atm": true,
    "pos": true,
    "contactless": true
  },
  "limits": {                                             // Optional — at least one amount must be > 0
    "transaction_enabled": true,
    "transaction": 5000,
    "daily_enabled": true,
    "daily": 20000,
    "monthly_enabled": true,
    "monthly": 100000,
    "yearly_enabled": false,
    "yearly": 0
  },
  "extras": {                                             // Optional - card extras
    "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)
}

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",  // Account ID (echoes request.account_id)
  "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": 5000,
        "daily_enabled": true,
        "daily": 20000,
        "monthly_enabled": true,
        "monthly": 100000,
        "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.

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 to default behavior
}

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": 5000,
        "daily_enabled": true,
        "daily": 20000,
        "monthly_enabled": true,
        "monthly": 100000,
        "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.

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": 10000,                               // Updated limit
    "daily_enabled": true,
    "daily": 50000,                                     // Updated limit
    "monthly_enabled": true,
    "monthly": 200000,                                  // Updated limit
    "yearly_enabled": false,
    "yearly": 0
  },
  "extras": {
    "auto_lock": "2024-12-31T23:59:59Z"
  }
}

Response

EditCardResponse(json)
{}

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.

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>