> ## Documentation Index
> Fetch the complete documentation index at: https://yn-c9bb3266.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Avoiding duplicates

> How Yuno guarantees that a retried API request does not create a duplicate resource. Verified against the production codebase.

Network failures, deploy restarts, and worker crashes are normal. The right pattern for a payment API is to make every retry safe to send again. Yuno's deduplication model is **business key first, idempotency header where supported**.

## Use unique business keys

Every write endpoint that creates a top level resource accepts an external identifier you control. Pick a stable, unique value for each logical operation. If a retry sends the same value, the database constraint rejects it with HTTP `400` instead of creating a duplicate.

| Resource                                | Field you set                                                            | Behavior on duplicate                                                                                                                                                                                                                                 |
| --------------------------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Customer (`POST /v1/customers`)         | `merchant_customer_id`                                                   | `400 CUSTOMER_ID_DUPLICATED`. Look up the existing record with [Retrieve Customer by External ID](/api-reference/customers/get-by-external-id).                                                                                                       |
| Payment (`POST /v1/payments`)           | `merchant_order_id` (on the checkout session, propagated to the payment) | Look up the existing payment with [Get Payment by Merchant Order](/api-reference/payments/get-by-merchant-order) before retrying. The orchestrator does not auto reject duplicate `POST /v1/payments` calls, so your retry strategy must check first. |
| Recipient (`POST /v1/recipients`)       | `merchant_recipient_id`                                                  | `400 EXTERNAL_ID_EXIST`. Fetch the existing recipient by merchant id.                                                                                                                                                                                 |
| Payout (`POST /v1/payouts`)             | `merchant_reference`                                                     | Treated as a unique reference. Check before retrying.                                                                                                                                                                                                 |
| Payment Link (`POST /v1/payment-links`) | `merchant_order_id`                                                      | Recommended unique value. No automatic dedupe at the API.                                                                                                                                                                                             |

```javascript Stable keys, safe retries theme={"theme":{"light":"github-dark","dark":"github-dark"}}
// Generate the merchant_customer_id once, persist it on your side,
// and reuse it on every retry of the same logical operation.
const merchantCustomerId = `cust-${order.userId}`;

await retryWithBackoff(() =>
  yuno.customers.create({ merchant_customer_id: merchantCustomerId })
);
```

<Note>
  Two simultaneous requests with the same business key can race. The first one wins. The loser receives the duplicate error. Treat that error as success and refetch the existing record by the same merchant id.
</Note>

## `X-Idempotency-Key` for subscriptions

`POST /v1/subscriptions` is the one endpoint that accepts the `X-Idempotency-Key` header today. The dedupe scope is **per account and per organization**, and it is **permanent** (a database unique index). On replay, the original `Subscription` response is returned without re creating the record.

```bash theme={"theme":{"light":"github-dark","dark":"github-dark"}}
curl --request POST \
  --url https://api-sandbox.y.uno/v1/subscriptions \
  --header 'Content-Type: application/json' \
  --header 'public-api-key: YOUR_PUBLIC_API_KEY' \
  --header 'private-secret-key: YOUR_PRIVATE_SECRET_KEY' \
  --header 'X-Idempotency-Key: sub-9f8a3c2e-4b1d-4e7a-a8c6-1f2e3d4c5b6a' \
  --data '{ /* subscription payload */ }'
```

| Aspect                                  | Behavior                                                                                   |
| --------------------------------------- | ------------------------------------------------------------------------------------------ |
| Header name                             | `X-Idempotency-Key`                                                                        |
| Required                                | No. Optional.                                                                              |
| Scope                                   | Per `account_id` and per organization.                                                     |
| Cache lifetime                          | Permanent. The key is stored in a database unique index, not a TTL cache.                  |
| Replay with same key                    | Returns the original subscription.                                                         |
| Replay with same key but different body | Returns the original subscription. The new body is **not** validated against the original. |

<Warning>
  Do not assume `X-Idempotency-Key` works on other endpoints. As of today it is only honored by `POST /v1/subscriptions`. On every other write endpoint, the header is ignored. Use the unique business key pattern above for those.
</Warning>

## Recommended client pattern

1. Generate a stable business key (`merchant_customer_id`, `merchant_order_id`, `merchant_recipient_id`) for every logical write operation. Persist it in your database before sending the request.
2. On retry, send the exact same key. If the server rejects with a duplicate error, look up the resource by that merchant id and treat the result as success.
3. For `POST /v1/subscriptions`, additionally send `X-Idempotency-Key` with a value derived from your subscription identifier.
4. Combine with exponential backoff and jitter on `408`, `429`, `500`, `502`, `503`, and `504`. See [Error handling](/core-concepts/error-handling).

## What next

<div className="mdx-card-tiles">
  <CardGroup cols={2}>
    <Card title="Error handling" icon="triangle-exclamation" href="/core-concepts/error-handling">
      The error envelope and which codes are safe to retry.
    </Card>

    <Card title="Authentication" icon="key" href="/getting-started/authentication">
      The three required headers and key rotation.
    </Card>

    <Card title="Get Customer by External ID" icon="user" href="/api-reference/customers/get-by-external-id">
      Look up a customer by your `merchant_customer_id`.
    </Card>

    <Card title="Get Payment by Merchant Order" icon="receipt" href="/api-reference/payments/get-by-merchant-order">
      Look up a payment by your `merchant_order_id`.
    </Card>
  </CardGroup>
</div>
