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

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

## 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**:


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


```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, 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


```python
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


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