Docs
Get sandbox access

Fincore · Guides

Send money to a tokenized card

Encrypt the card number on your side, exchange it for a token, register the token as the destination instrument and send it Money Out without storing the card number.

Use this guide when you need to pay out to a card but do not want to store or send the card number. You encrypt the card number on your side, Monato stores it in its card vault and gives you a token. From then on you only use the token: you register it as the destination instrument and send Money Out to it.

The Money Out operation is the same one you use for CLABEs and debit cards. Only the first two steps are new.

Flow

Step Action Operation Save
1 Get a bearer token POST /v1/clients/{clientId}/auth/credential-tokens (Authentication) Bearer token.
2 Encrypt the card number and tokenize it Tokenize a card token_id, first_6, last_4.
3 Register the token as the destination Register instrument with card_token Destination instrument id.
4 Send Money Out Create Money Out transaction Transaction id, trackingId, initial status.
5 Reconcile the status Status update webhook Final transaction state.

Keep only token_id, first_6 and last_4. Delete the card number after step 2 and never log it, nor the JSON you encrypt.

1. Get your encryption key

Monato gives you one public key per environment, with its kid (key name, for example stg-v1) and its SHA-256 fingerprint. The public key only encrypts. The private key that decrypts stays in Monato’s key service and is never exported. During a key rotation two kid values are active at the same time.

Store the public key as versioned configuration and keep your servers’ clocks synchronized (the request time must be within 30 seconds in the future).

2. Tokenize the card

Build this JSON for each card:

Field Rule
pan Required. 15 to 19 digits. Must pass the Luhn check.
tenant_id Required. Your client_id. It must match the client of the bearer token.
nonce Required. 16 to 128 characters, new for every request. Use a UUID v4.
iat Optional, recommended. Current time in epoch seconds. If you send it, it must be from 300 seconds in the past to 30 seconds in the future.
holder_name Optional. Accepted and discarded.
JSON before encryption
{
  "pan": "4111111111111111",
  "tenant_id": "<your client_id>",
  "nonce": "3f6c1a52-8e0d-4b7a-9c21-5d4e7f8a9b10",
  "iat": 1791916421
}

Do not add any other field. Any undeclared field (for example a CVV) rejects the whole request with 400.

Encrypt the JSON as a compact JWE with alg = RSA-OAEP-256, enc = A256GCM and the kid in the protected header, and send it as the body of Tokenize a card:

curl -X POST https://apicore.stg.finch.lat/v1/clients/$CLIENT_ID/card-tokens \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/jose" \
  --data "$JWE"

The response has Cache-Control: no-store. Every call creates a new token, even when the card was tokenized before. The JWE can be up to 8192 characters.

Your JWE library does the encryption in a few lines. You do not implement cryptography yourself.

import { readFileSync } from 'node:fs';
import { randomUUID } from 'node:crypto';
import { CompactEncrypt, importSPKI } from 'jose';

const pem = readFileSync('monato-stg-v1.pem', 'utf8'); // public key from Monato
const kid = 'stg-v1';                                 // kid delivered with the key
const payload = { pan: '4111111111111111', tenant_id: '<your client_id>', nonce: randomUUID(), iat: Math.floor(Date.now() / 1000) };

const key = await importSPKI(pem, 'RSA-OAEP-256');
const jwe = await new CompactEncrypt(new TextEncoder().encode(JSON.stringify(payload)))
  .setProtectedHeader({ alg: 'RSA-OAEP-256', enc: 'A256GCM', kid })
  .encrypt(key);
// jwe is the body of POST /v1/clients/{clientId}/card-tokens

Tokenizer errors

HTTP code What to do
400 SCHEMA_VALIDATION_ERROR Malformed JWE, unknown kid, payload that breaks the rules above (including an invalid card number or a tenant_id that is not yours). Fix the request; do not retry it as is.
400 IAT_OUT_OF_WINDOW Check your clock and build a new JWE.
401 UNAUTHORIZED Renew the bearer token.
409 NONCE_REPLAY The nonce was already used. Build a new JWE with a new nonce.
415 SCHEMA_VALIDATION_ERROR Send Content-Type: application/jose.
503 INTERNAL_ERROR Temporary failure. Retry with backoff and a new JWE (new nonce and iat).

SCHEMA_VALIDATION_ERROR deliberately does not say which check failed.

3. Register the token as the destination

Send a card_token object instead of debit_card or virtual_clabe. You do not send the card number or its first and last digits: Monato reads them from the vault.

Field Rule
card_token.destination_bank_id Required. Bank UUID of the card issuer. It must match the card’s first six digits.
card_token.token_id Required. The token_id from step 2, in UUID format. It must belong to your client.
card_token.holder_name Required. Up to 40 characters.
type Must be RECEIVER. A tokenized card can only receive.
customer_id Optional. Send it when the card belongs to a Business Unit.
curl -X POST https://apicore.stg.finch.lat/v1/clients/$CLIENT_ID/instruments \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "source_bank_id": "9d84b03a-28d1-4898-a69c-38824239e2b1",
    "type": "RECEIVER",
    "rfc": "XAXX010101000",
    "alias": "Card ABC123",
    "card_token": {
      "destination_bank_id": "3054ff18-32a0-478d-b9fe-b5261f9a6e1f",
      "token_id": "9f1b2c3d-4e5f-4a6b-8c7d-1234567890ab",
      "holder_name": "Jane Doe"
    }
  }'

The response id is your destination_instrument_id. For a tokenized card, instrumentDetail returns tokenId, first6, last4 and holderName. It never returns a card number.

Registration fails with 400 for a malformed token_id, a type other than RECEIVER, an unsupported BIN or a destination bank that does not match the card, and with 404 if the token does not exist or belongs to another client. If the card vault is temporarily unavailable, no instrument is created; retry later.

4. Send Money Out

Use the instrument id as destination_instrument_id in Create Money Out transaction. Everything else works exactly as in Send money to a CLABE: the same request fields, Idempotency-Key, status webhook and errors.

curl -X POST https://apicore.stg.finch.lat/v1/transactions/money_out \
  -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
    "source_instrument_id": "709448c3-7cbf-454d-a87e-feb23801269a",
    "destination_instrument_id": "5b0c6a1e-7d2f-4e8a-9c3b-2a1d0e9f8c7b",
    "transaction_request": {
      "external_reference": "1234567",
      "description": "Customer payout",
      "amount": "250.00",
      "currency": "MXN"
    }
  }'

The transaction starts as INITIALIZED. The final state arrives through the Status update webhook; see Transaction statuses.