DevelopersBuild safely

Idempotency

Every v1 write needs an Idempotency-Key. How keys are kept for 24 hours, what a replay returns, and the five idempotency errors.

2 min read

Every v1 write needs an Idempotency-Key header. Today that is the five POST endpoints: create a checkout session, cancel it, reissue its link, cancel a subscription and send a test event. If a request fails on the network, you send it again with the same key, and Fianto answers with the first response instead of doing the work twice.

This matters most for creating a checkout session: its url comes back only in the create response. A retry under the same key returns that stored response, url included.

The key

  • 1 to 255 printable ASCII characters, with no spaces. Keys are separate per application.
  • Kept for 24 hours. Within that time, the same key with the same request (method, path, body and query) returns the stored response, with the header Idempotent-Replayed: true.
  • If the first request failed with an error, the key is released, and you can use it again.
curl https://api.fianto.xyz/v1/checkout-sessions/SESSION_ID/cancel \
  --user "$FIANTO_APP_ID:$FIANTO_APP_SECRET" \
  -H "Idempotency-Key: cancel-SESSION_ID-1" \
  -X POST

In the SDK

The SDK sends a key with every POST on its own: a crypto.randomUUID() made once per call and reused on every automatic retry of that call. That covers a lost response while your process keeps running.

If your process can crash between creating a session and saving its url, derive the key from data you already have, so a restarted process sends the same key:

import { Fianto } from '@fianto/sdk';

const fianto = new Fianto();

export async function createCheckout(orderId: string, attempt: number) {
  return fianto.checkoutSessions.create(
    {
      mode: 'payment',
      order_id: orderId,
      amount: '12.50',
      description: 'Coffee beans, 1 kg',
      success_url: 'https://shop.example/thank-you',
      cancel_url: 'https://shop.example/cart',
    },
    // The same order and attempt always send the same key.
    { idempotencyKey: `checkout:${orderId}:${attempt}` },
  );
}

When the SDK gives up on a network failure, the ConnectionError or TimeoutError carries the idempotencyKey it used. Retry the same call with that key to recover:

import { ConnectionError, Fianto, TimeoutError } from '@fianto/sdk';
import type { CheckoutSessionCreateParams } from '@fianto/sdk';

const fianto = new Fianto();

export async function createWithRecovery(params: CheckoutSessionCreateParams) {
  try {
    return await fianto.checkoutSessions.create(params);
  } catch (err) {
    if ((err instanceof ConnectionError || err instanceof TimeoutError) && err.idempotencyKey) {
      return fianto.checkoutSessions.create(params, { idempotencyKey: err.idempotencyKey });
    }
    throw err;
  }
}

A new key is a new request

Retrying with a fresh key runs the request again. For a create whose first attempt did succeed, the second call finds the order's open session and returns it with url: null, so you lose the link until you reissue it. Reuse the key whenever you repeat a write.

Troubleshooting

You seeWhyFix
400 idempotency_key_requiredA POST was sent without an Idempotency-Key header, or with an empty one.Add the header. The SDK does this for you.
400 idempotency_key_invalidThe key is longer than 255 characters, or has spaces or non-printable characters.Use 1 to 255 printable ASCII characters with no spaces.
422 idempotency_key_reusedThe key was already used with a different request.Use a new key for a new request. The SDK does not retry this error.
409 idempotency_request_in_progressThe first request with this key is still running.Retry with the same key in a few seconds. The SDK retries this one.
409 idempotency_response_unreadableThe request already completed, but its saved response can no longer be read.Fetch the resource to see the result, for example GET /v1/orders/lookup?order_id=. Do not resend under a new key without checking first.

See also

Was this page helpful? Tell us

On this page