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 payload mismatch. | 409 Conflict | Payload 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. |
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-199819064afa
Authorization: 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 payload mismatch",
"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, 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 request JSON with keys sorted (canonicalized)
Formula
name = client_id + method + body_hash
Idempotency-Key = UUIDv5(namespace, name)
import json
import hashlib
import uuid
# Namespace UUID assigned by FINCO (varies by environment)
NAMESPACE_IDEMPOTENCY = uuid.UUID("086fc9ec-d591-4045-bde4-3f9439506b08")
def calculate_body_hash(data: dict) -> str:
"""
Calculates the SHA-256 hash of the body, ensuring the order of the keys.
"""
json_string = json.dumps(data, sort_keys=True, separators=(',', ':'))
return hashlib.sha256(json_string.encode("utf-8")).hexdigest()
def generate_idempotency_key(client_id: str, method: str, body_hash: str) -> str:
"""
Generates the deterministic Idempotency-Key in UUID v5 format.
"""
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"
body_hash = calculate_body_hash(request_body)
idempotency_key = generate_idempotency_key(client_id, method, body_hash)
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");
const NAMESPACE_IDEMPOTENCY = "086fc9ec-d591-4045-bde4-3f9439506b08";
function calculateBodyHash(data) {
const jsonString = JSON.stringify(
Object.keys(data)
.sort()
.reduce((obj, key) => ((obj[key] = data[key]), obj), {})
);
return crypto.createHash("sha256").update(jsonString).digest("hex");
}
function generateIdempotencyKey(clientId, method, bodyHash) {
return uuidv5(clientId + method + bodyHash, NAMESPACE_IDEMPOTENCY);
}
const method = "money_out";
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"
const bodyHash = calculateBodyHash(requestBody);
const idempotencyKey = generateIdempotencyKey(clientId, method, bodyHash);
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.JSON key order
The request body must be serialized with keys sorted alphabetically (
sort_keys=True
). This ensures the hash is identical even if the original JSON key order varies.Client–Server consistency
The
body_hash
calculation 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.