---
title: Best practices
description: Practices for a reliable Clipia API integration — idempotent retries, webhooks over polling, error and 429 handling with backoff, cost estimation, and storing request_id.
---

To make your Clipia API integration reliable and predictable on cost, follow five practices: make submissions idempotent, prefer webhooks over polling, handle errors and `429` with exponential backoff, estimate cost before submitting, and store the `request_id` on your side. Each practice is covered below.

## Idempotent retries

Send an `Idempotency-Key` (a unique UUID v4 per **logical** request) on every submit. If the network drops, retry with the same key and the same parameters — you get the **same** `request_id` back, with no duplicate generation and no second charge. The key is stored for 24 hours.

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

| Scenario | Result |
|----------|--------|
| same key + same parameters | the same `request_id`, no second charge |
| same key + different parameters | `409 idempotency_key_reuse` |
| retry while the first is still processing | `409 request_in_progress` |

<Callout type="warn" title="One key per logical request">
Generate a fresh UUID v4 for each new generation, not for each network retry. Never put sensitive data (an email and so on) in the key.
</Callout>

## Webhooks over polling

Pass a `webhook_url` on submit — Clipia sends a `POST` when the job finishes, so you don't have to poll. Keep polling as a fallback. If you do poll, do it sensibly (say, once every 1–3 seconds), not in a tight loop.

- Respond `2xx` within 10 seconds, or delivery is retried (up to 6 attempts with exponential backoff).
- Verify the `X-Clipia-Signature` (HMAC-SHA256) on every delivery.
- Handle deliveries **idempotently** by `request_id` — redelivery is possible.

## Error and 429 handling with backoff

Errors arrive in a single JSON shape with `error.type`, `error.code`, and `error.message`. React by HTTP status.

| HTTP | `code` | What to do |
|------|--------|-----------|
| `400` / `422` | `invalid_request` / `model_input_invalid` | fix the body/parameters; don't retry as-is |
| `401` / `403` | `invalid_api_key` / `insufficient_scope` | check the key and scope |
| `402` | `insufficient_credits` | top up the balance |
| `429` | `rate_limit_exceeded` | wait `Retry-After` seconds, then retry |
| `500` / `503` | `internal_error` / `service_unavailable` | retry with exponential backoff |

On `429`, follow the `Retry-After` header and the `RateLimit-*` metrics:

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

<Callout type="info" title="Exponential backoff with jitter">
For `429`, `500`, and `503`, retry with a growing pause and random jitter (e.g. 1s, 2s, 4s, 8s… plus a random add-on) to avoid thundering herds. Default limits are 120 RPM and 10 concurrent generations per key.
</Callout>

## Estimate cost before submitting

The operation price is fixed and known up front. To learn the cost of a specific parameter set without queuing, use `POST /v1/models/{model}/estimate` — it returns `credits`. This lets you show the cost to a user or reject expensive requests early. On submit the price is echoed in the `cost` field; if credits are short, submit returns `402`.

## Store the request_id

Persist each submission's `request_id` in your database right after the `submit` response. You need it to:

- match an incoming webhook to your entity;
- poll `status_url` / `response_url` when needed;
- enforce idempotency on your side (don't process a delivery twice).

<Callout type="warn" title="Pair request_id with Idempotency-Key">
Store the pair `Idempotency-Key` → `request_id`: if the `submit` response is lost, a retry with the same key returns the same `request_id`, so you never create a duplicate generation.
</Callout>
