# Idempotency 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 ## Enable Idempotency Add the header below to any request you want to make idempotent. **Header**: `Idempotency-Key: ` **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. ### Idempotency behavior (quick reference) | 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. | ## Money Out — Using Idempotency With your default source Instrument and a valid destination Instrument, send Money Out requests with the `Idempotency-Key` header to make retries safe. **Endpoint** `POST /v1/transactions/money_out` ### Successful request (cached for 24h) **Request** **Path parameters**: none **Query Parameters**: none **Headers**: `Idempotency-Key: 66c0b04f-97d6-592d-8396-199819064afa` `Authorization: Bearer ` `Content-Type: application/json` **Request Body**: ```json { "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**: ```json { "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. ### Mismatch example (same key, different body) If you reuse the **same** `Idempotency-Key` but change the **body** (e.g., a different `amount`), the request is rejected. **Request Body**: ```json { "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): ```json { "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" } } ] } ``` ### In-progress example (near-simultaneous duplicates) 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): ```json { "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" } } ] } ``` ## Deterministic Key Creation (UUID v5) 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) ``` ### Python — sample ```python 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` ### Node.js — sample ```js 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` ### Important notes - **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`, or `body`), the resulting UUID will also change.