Skip to content
Last updated

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: <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.

Idempotency behavior (quick reference)

ScenarioTypical responseNotes
No Idempotency-Key2xx / 4xx / 5xxTraditional flow (no idempotency).
Invalid UUID format for idempotency-key400 Bad RequestInput validation error.
Idempotency key mismatch (payload or namespace)409 ConflictPayload 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.


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 <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.


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:

{
  "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"
      }
    }
  ]
}

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):

{
  "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, 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.

Python — sample

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

Node.js — sample

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

Important notes

  • 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_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.