---
title: Best practices
description: Build a reliable Clipia API integration — idempotent retries, webhooks over polling, error and 429 handling, cost estimates and storing request_id.
icon: BadgeCheck
---

A reliable Clipia API integration with predictable costs comes down to five practices: make submissions idempotent, prefer webhooks to polling, handle errors and `429` with backoff, estimate the cost before you submit, and store `request_id` on your side.

## Idempotent retries [#idempotency]

Send an `Idempotency-Key` with every submission — a unique UUID v4 per **logical** request. If the connection drops, retry with the same key and the same body: you get the **same** `request_id` back, with no second generation and no second charge. A key lives for 24 hours within a single API key.

```bash
curl -X POST https://api.clipia.ai/v1/models/nano-banana-2 \
  -H "Authorization: Bearer $CLIPIA_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 8f3a1c7e-2b41-4d2a-9c0e-1200304cf45b" \
  -d '{ "input": { "prompt": "a sunset over mountains, cinematic" } }'
```

| Situation | Result |
| --- | --- |
| Same key, same body | Same `request_id`, no second charge |
| Same key, different model or body | `409 idempotency_key_reuse` |
| Retry while the first request is still being processed | `409 request_in_progress` — retry in a few seconds |

<Callout type="warn" title="One key per logical request">
Create a new UUID for every new generation, not for every network attempt. The SDKs add a random key on their own, but to retry safely, pass your own key and reuse it. Don't put personal data in the key. See [Idempotency](/en/docs/api-reference/idempotency).
</Callout>

## Webhooks over polling [#webhooks]

Pass a `webhook_url` (`https://` only) when you submit, and Clipia sends a `POST` when the job finishes, so you don't have to poll. Keep polling as a fallback and poll no more than once every 1–3 seconds.

- Respond with `2xx` within 10 seconds, otherwise the delivery is retried — up to 15 attempts with growing delays, for about 24 hours.
- Verify the `X-Clipia-Signature` (HMAC-SHA256) on every delivery. Each API key has its own webhook secret.
- Handle deliveries **idempotently** by `request_id`: the same delivery can arrive more than once.

See [Webhooks](/en/docs/api-reference/webhooks).

## Errors and 429 [#errors]

Errors share one format: an `error` object with `type`, `code` and `message`. Decide what to do from the HTTP status and `code`.

| HTTP | `code` | What to do |
| --- | --- | --- |
| `400` / `422` | `invalid_request` / `model_input_invalid` | Fix the body or parameters; don't retry as is |
| `401` / `403` | `invalid_api_key` / `insufficient_scope` | Check the key and its permissions |
| `402` | `insufficient_credits` | Top up the balance |
| `409` | `request_in_progress` | Retry in a few seconds with the same idempotency key |
| `429` | `rate_limit_exceeded` | Wait `Retry-After` seconds and retry |
| `429` | `concurrency_limit_exceeded` | Wait for one of your jobs to finish |
| `500` / `503` | `internal_error` / `service_unavailable` | Retry with backoff |

On `429`, rely on `Retry-After` and the `RateLimit-*` headers. `RateLimit-Reset` is the moment the window resets, in Unix seconds, not the number of seconds until then:

```http
RateLimit-Limit: 120
RateLimit-Remaining: 0
RateLimit-Reset: 1760011260
Retry-After: 37
```

<Callout type="info" title="Exponential backoff with jitter">
For `429`, `500` and `503`, retry with growing delays plus random jitter — for example 1, 2, 4, 8 seconds, at most five attempts — so retries from different clients don't line up. Default limits are 120 requests per minute per key and 600 across all keys of an account. A concurrency limit applies only if one is set for the key or the account. See [Rate limits](/en/docs/api-reference/rate-limits).
</Callout>

## Estimate cost before submitting [#estimate]

To price a specific set of parameters without queuing anything, call `POST /v1/models/{model}/estimate` — it returns `credits`. Use it to show the price to your users or to block expensive requests up front. On submission, the `cost` field holds the amount reserved for the job; if the balance is too low, the submission returns `402`. See [Cost estimation](/en/docs/api-reference/estimate).

<Callout type="info" title="Pricing">
Model prices change together with the catalog: `estimate` returns the exact amount in credits for your parameters, and on submit it arrives in the `cost` field before anything is charged.
</Callout>

## Store the request_id [#request-id]

Save the `request_id` of every submission in your database right after the response. You need it to:

- match an incoming webhook to your own entity;
- poll `status_url` or `response_url` when needed;
- avoid processing the same delivery twice.

<Callout type="warn" title="Pair Idempotency-Key with request_id">
Store the `Idempotency-Key` → `request_id` pair. If the response to a submission is lost, retrying with the same key returns the same `request_id`, and no duplicate generation appears.
</Callout>
