Idempotency guarantees that multiple submissions of the same request result in a single effect and the same response. It prevents duplicates and makes retries safe and predictable.
Scope: applies to endpoints that support it. Today: Money Out
TTL: 24 hours from the first request
Add the header below to any request you want to make idempotent.
Header:Idempotency-Key: <uuid-v5>
Notes
- If you don’t send the header, the request behaves as usual (no idempotency).
- The first response (success or error) is cached for 24h; identical retries return the same response from cache.
- The same key must be reused with the same body. If the body changes, generate a new key.
| Scenario | Typical response | Notes |
|---|---|---|
No Idempotency-Key | 2xx / 4xx / 5xx | Traditional flow (no idempotency). |
| Invalid UUID format for idempotency-key | 400 Bad Request | Input validation error. |
| Idempotency key mismatch (payload or namespace) | 409 Conflict | Payload or namespace does not match cached key. |
| Operation money_out in progress. | 409 Conflict (operation_in_progress) | Near-simultaneous duplicate. |
| Transaction amount format is invalid. | 400 Bad Request (cached) | First error is cached for TTL. |
| Missing resource (e.g., instrument not found) | 500 Internal Server Error (cached) | First error is cached for TTL. |
Internally, idempotency keys are stored in an in-memory layer (Valkey/Redis-compatible) with a time-to-live (TTL) of 24 hours. The first result, whether a success (2xx) or an error (4xx/5xx), is associated with the key and reused for all identical retries during that period.
With your default source Instrument and a valid destination Instrument, send Money Out requests with the Idempotency-Key header to make retries safe.
EndpointPOST /v1/transactions/money_out
Request
Path parameters: none
Query Parameters: none
Headers:Idempotency-Key: 66c0b04f-97d6-592d-8396-199819064afaAuthorization: Bearer <token>Content-Type: application/json
Request Body:
{
"client_id": "c2d1d1e3-3340-4170-980e-e9269bbbc551",
"source_instrument_id": "709448c3-7cbf-454d-a87e-feb23801269a",
"destination_instrument_id": "dd7f8d89-94dd-43ca-871b-720fde378b52",
"transaction_request": {
"external_reference": "7654329",
"description": "lorem ipsum dolor sit amet",
"amount": "1.95",
"currency": "MXN"
}
}Response
Status Code: 200 OK
Response Body:
{
"id": "16811ee8-1ef9-4dd4-8d84-9c2df89cf302",
"bankId": "9d84b03a-28d1-4898-a69c-38824239e2b1",
"clientId": "c2d1d1e3-3340-4170-980e-e9269bbbc551",
"externalReference": "7654329",
"trackingId": "20250306FINCHVLIKQ5SKUM",
"description": "lorem ipsum dolor sit amet",
"amount": "1.95",
"currency": "MXN",
"category": "DEBIT_TRANS",
"subCategory": "SPEI_DEBIT",
"transactionStatus": "INITIALIZED",
"audit": {
"createdAt": "2025-03-06 11:57:55.408000-06:00",
"updatedAt": "2025-03-06 11:57:55.408000-06:00",
"deletedAt": "None",
"blockedAt": "None"
}
}Identical retry behavior
Send the same request again within 24h using the same Idempotency-Key and same body.
Status Code: 200 OK (served from cache) — same body as above.
If you reuse the same Idempotency-Key but change the body (e.g., a different amount), the request is rejected.
Request Body:
{
"client_id": "c2d1d1e3-3340-4170-980e-e9269bbbc551",
"source_instrument_id": "709448c3-7cbf-454d-a87e-feb23801269a",
"destination_instrument_id": "dd7f8d89-94dd-43ca-871b-720fde378b52",
"transaction_request": {
"external_reference": "7654329",
"description": "lorem ipsum dolor sit amet",
"amount": "2.10",
"currency": "MXN"
}
}Response
Status Code: 409 Conflict
Response Body (example):
{
"code": 6,
"message": "API Error",
"details": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"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",
"module": "Transactions",
"method_name": "RegisterMoneyOut",
"error_code": "10-E4001"
}
}
]
}
If two identical requests arrive nearly at the same time, the first proceeds; the second returns “in progress”.
Response
Status Code: 409 Conflict
Response Body (example):
{
"code": 6,
"message": "API Error",
"details": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "UNIQUE_VIOLATION",
"domain": "CORE",
"metadata": {
"error_detail": "Operation money_out in progress",
"http_code": "409",
"module": "Transactions",
"method_name": "RegisterMoneyOut",
"error_code": "10-E4001"
}
}
]
}
The idempotency key is client-generated and deterministic so the same (client_id, method, canonical body) yields the same key.
Format: UUID v5 (name-based)
Namespace: UUID provided per environment (QA/Stg/Prod)
Method: public alias, today: "money_out"
Body Hash: SHA-256 of the canonical JSON request body, with object keys sorted alphabetically at every level
Formula
name = client_id + method + body_hash
Idempotency-Key = UUIDv5(namespace, name)The request body must be canonicalized before hashing. All object keys, including nested objects, must be sorted alphabetically and serialized consistently so the same payload always produces the same hash.
import json
import hashlib
import uuid
"""
Fixed namespace by environment used to generate deterministic UUIDv5 idempotency keys.
UUIDv5 guarantees that the same inputs always generate the same output.
"""
NAMESPACE_IDEMPOTENCY = uuid.UUID("086fc9ec-d591-4045-bde4-3f9439506b08")
def calculate_body_hash(data: dict) -> str:
"""
Generates a SHA-256 hash of the request body.
IMPORTANT:
The body keys are sorted alphabetically before serialization.
This ensures a deterministic JSON representation of the payload.
Without sorting, two semantically identical bodies with different
key orders would produce different hashes and therefore different
idempotency keys.
The separators argument removes unnecessary whitespace so the
serialized JSON remains consistent across environments.
"""
json_string = json.dumps(data, sort_keys=True, separators=(',', ':'))
# Generate SHA-256 hash of the normalized JSON string
return hashlib.sha256(json_string.encode("utf-8")).hexdigest()
def generate_idempotency_key(client_id: str, method: str, body_hash: str) -> str:
"""
Generates the final idempotency key.
The key is derived from:
- client_id (caller identity)
- method (API operation)
- body_hash (deterministic representation of the request body)
Because UUIDv5 is deterministic, identical inputs will always produce
the same idempotency key, enabling safe request retries.
"""
return str(uuid.uuid5(NAMESPACE_IDEMPOTENCY, client_id + method + body_hash))
# Usage Example:
method = "money_out"
request_body = {
"client_id": "b000654b-4d12-46e5-b451-662459b6effc",
"source_instrument_id": "83fe58c6-15ad-4dd5-a4f2-ae7e5b39753a",
"destination_instrument_id": "206509fc-f879-4fa7-b6b1-243073fd94e3",
"transaction_request": {
"amount": "0.01",
"currency": "MXN",
"description": "FINCO PAY CTA MENSUAL SPEI",
"external_reference": "1236"
}
}
client_id = "b000654b-4d12-46e5-b451-662459b6effc"
# Step 1: Generate deterministic hash of the normalized request body
body_hash = calculate_body_hash(request_body)
# Step 2: Generate deterministic idempotency key
idempotency_key = generate_idempotency_key(client_id, method, body_hash)
# This value should be sent in the request header: Idempotency-Key
print(f"Idempotency-Key: {idempotency_key}")Expected output (with the sample above)Idempotency-Key: 66c0b04f-97d6-592d-8396-199819064afa
const crypto = require("crypto");
const { v5: uuidv5 } = require("uuid");
/*
Fixed namespace by environment used to generate deterministic UUIDv5 idempotency keys.
UUIDv5 guarantees that the same inputs always generate the same output.
*/
const NAMESPACE_IDEMPOTENCY = "086fc9ec-d591-4045-bde4-3f9439506b08";
/*
Recursively sorts all object keys alphabetically.
This ensures nested objects are normalized before hashing.
*/
function sortObjectRecursively(value) {
if (Array.isArray(value)) {
return value.map(sortObjectRecursively);
}
if (value !== null && typeof value === "object") {
return Object.keys(value)
.sort()
.reduce((result, key) => {
result[key] = sortObjectRecursively(value[key]);
return result;
}, {});
}
return value;
}
/*
Generates a SHA-256 hash of the request body.
IMPORTANT:
The body keys are sorted alphabetically before serialization.
This ensures a deterministic JSON representation of the payload.
Without sorting, two semantically identical bodies with different
key orders would produce different hashes and therefore different
idempotency keys.
*/
function calculateBodyHash(data) {
const normalizedBody = sortObjectRecursively(data);
const jsonString = JSON.stringify(normalizedBody);
// Generate SHA-256 hash of the normalized JSON string
return crypto.createHash("sha256").update(jsonString).digest("hex");
}
/*
Generates the final idempotency key.
The key is derived from:
- clientId (caller identity)
- method (API operation)
- bodyHash (deterministic representation of the request body)
Because UUIDv5 is deterministic, identical inputs will always produce
the same idempotency key, enabling safe request retries.
*/
function generateIdempotencyKey(clientId, method, bodyHash) {
return uuidv5(clientId + method + bodyHash, NAMESPACE_IDEMPOTENCY);
}
// API method
const method = "money_out";
/*
Example request body.
Note: The original key order does not matter because calculateBodyHash()
will normalize it before hashing.
*/
const requestBody = {
client_id: "b000654b-4d12-46e5-b451-662459b6effc",
source_instrument_id: "83fe58c6-15ad-4dd5-a4f2-ae7e5b39753a",
destination_instrument_id: "206509fc-f879-4fa7-b6b1-243073fd94e3",
transaction_request: {
amount: "0.01",
currency: "MXN",
description: "FINCO PAY CTA MENSUAL SPEI",
external_reference: "1236",
},
};
const clientId = "b000654b-4d12-46e5-b451-662459b6effc";
// Step 1: Generate deterministic hash of the normalized request body
const bodyHash = calculateBodyHash(requestBody);
// Step 2: Generate deterministic idempotency key
const idempotencyKey = generateIdempotencyKey(clientId, method, bodyHash);
// This value should be sent in the request header: Idempotency-Key
console.log(`Idempotency-Key: ${idempotencyKey}`);Expected output (with the sample above)Idempotency-Key: 66c0b04f-97d6-592d-8396-199819064afa
Namespace per environment
Each environment (QA, Staging, Production) has its own
NAMESPACE_IDEMPOTENCY. This prevents collisions between keys generated across environments.Canonical JSON serialization
The request body must be canonicalized before hashing. All object keys, including nested objects, must be sorted alphabetically and serialized consistently. This ensures the hash is identical even if the original JSON key order varies.
Client–Server consistency
The
body_hashcalculation must be identical to what FINCO’s backend uses. Make sure you use the same serialization logic and hashing algorithm.Guaranteed uniqueness
If any of the three components change (
client_id,method, orbody), the resulting UUID will also change.