Docs
Get sandbox access

General · Platform basics

Idempotency

Retry requests safely with idempotency keys in Fincore, Direct Debit, Gift Cards and Billpay.

A network timeout does not tell you whether your request ran. An idempotency key lets you retry without paying, charging or buying twice. Each product implements it differently.

Compare products

Product Field Required Operations Key format Expiry
Fincore Idempotency-Key header No, but always send it in production Create Money Out UUID v5 24 hours
Direct Debit Idempotency-Key header No Create charge UUID 24 hours
Gift Cards idempotency_key in the body Yes Purchase a gift card String Not documented
Billpay (v1) idempotency_key Optional on payments and top-ups, required on verify payment Create payment, Create top-up, Verify payment String Not documented

What happens when you retry

Scenario Fincore Direct Debit Gift Cards
No key Processed normally, without retry protection Processed normally Rejected. The key is required.
Invalid key format Rejected (not a UUID) 400 Bad Request —
Same key, same body Original response returned Original charge returned Rejected with DUPLICATED_PAYMENT_ERROR
Same key, different body 409 conflict: the key does not match the body 422 Unprocessable Entity Rejected with DUPLICATED_PAYMENT_ERROR
Same key while the first request is still running 409 conflict 409 Conflict. Wait and retry. —
Same key after the first request failed The same error is returned until the key expires — —
Same key after it expires Processed as a new request Creates a new charge —

Gift Cards does not return the original purchase on a repeated key. It rejects the request instead.

Fincore only applies the key to Create Money Out. Other Fincore operations, including Penny Validation, ignore the header.

Fincore: generate the key from the request

Fincore does not accept any UUID. It recomputes the key from the request it receives and rejects the request with 409 if your key is different. Compute it like this:

Formula
Idempotency-Key = UUIDv5(NAMESPACE, client_id + "money_out" + SHA256_hex(canonical_json(body)))
Input Rule
NAMESPACE A UUID for each environment. Monato gives it to you during onboarding, one for staging and another for production. It keeps keys from different environments apart.
client_id The client_id in the request body.
"money_out" The operation name, exactly as written.
SHA256_hex(...) SHA-256 of the canonical body, as 64 lowercase hexadecimal characters.

Join client_id, money_out and the hash with no separators.

Build canonical_json(body) from the same body you send:

  • Sort object keys alphabetically at every level.
  • Use no whitespace: , between items and : between key and value.
  • Escape every non-ASCII character as \uXXXX with lowercase hex (for example ñ becomes \u00f1). JavaScript’s JSON.stringify does not do this, so escape them yourself.
  • Include only the fields you send with a value. Empty fields, such as "client_reference": "", are dropped before the hash is computed.
  • Keep strings as strings, for example "amount": "250.00".
import hashlib, json, uuid

NAMESPACE = uuid.UUID("00000000-0000-0000-0000-000000000000")  # use the namespace Monato gives you

def idempotency_key(body: dict) -> str:
    canonical = json.dumps(body, sort_keys=True, separators=(",", ":"))
    body_hash = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
    return str(uuid.uuid5(NAMESPACE, body["client_id"] + "money_out" + body_hash))

Check your implementation with this test vector. With the example namespace 00000000-0000-0000-0000-000000000000, this body must produce 9b382639-c68a-563c-9f3a-bda225eb2512:

{
  "client_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
  "source_instrument_id": "709448c3-7cbf-454d-a87e-feb23801269a",
  "destination_instrument_id": "dd7f8d89-94dd-43ca-871b-720fde378b52",
  "transaction_request": {
    "external_reference": "1234567",
    "description": "Pago a proveedor Peña",
    "amount": "250.00",
    "currency": "MXN"
  }
}

Fincore keeps the result for 24 hours, including failures. If the first request failed, for example because of insufficient balance, retrying with the same key returns the same error. After you fix the cause, change the body (for example external_reference), which gives a new key.

A conflict returns HTTP 409 with UNIQUE_VIOLATION. The key does not match the body, or the namespace is wrong:

409 key does not match
{
  "code": 6,
  "message": "API Error",
  "details": [
    {
      "reason": "UNIQUE_VIOLATION",
      "domain": "CORE",
      "metadata": {
        "error_detail": "Idempotency key does not match the request payload or the namespace may be incorrect",
        "http_code": "409"
      }
    }
  ]
}

The first request with the same key is still running. Wait and retry with the same key and body:

409 request in progress
{
  "code": 6,
  "message": "API Error",
  "details": [
    {
      "reason": "UNIQUE_VIOLATION",
      "domain": "CORE",
      "metadata": {
        "error_detail": "Operation money_out in progress",
        "http_code": "409"
      }
    }
  ]
}

Direct Debit: send a UUID

Send any UUID in the Idempotency-Key header of POST /charges. Keys are scoped to your organization.

Direct Debit charge with a key
curl -X POST https://stg.directdebit.monato.com/charges \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Idempotency-Key: 00000000-0000-4000-8000-000000000000" \
  -d '{ ... }'

Lottery

Lottery has no idempotency key, but each ticket purchase must use a unique payer_reference.