Idempotency
You know when a retry is the same logical operation; we don't. Pass idempotencyKey on a create or update and, for 24 hours, sending the same request again with that key gets the first
successful response back, not a duplicate row. A key stands for one request, so the same key with a
different body is refused.
How it wires up
Most create and update calls, and sub-resource .create calls, accept an options bag with idempotencyKey. The client adds it to the Idempotency-Key header before sending. Two creates don't take one yet: analytics.csatSurveys.create and community.posts.createComment. The
SDK doesn't retry those, so check before you send one again. Updates to members, custom fields
and settings (statuses, quick replies, SLA rules, preferences, business hours, the widget) don't
take a key either; sending the same update twice leaves the same result.
await cstar.tickets.create(
{ title: 'Need help', priority: 'high' },
{ idempotencyKey: 'submission_form_42' }
);
// Same key and same body inside 24h returns the first response. No duplicate ticket.
await cstar.tickets.create(
{ title: 'Need help', priority: 'high' },
{ idempotencyKey: 'submission_form_42' }
); What the API does with the key
- Same key, same request: the first successful response comes back, with
Idempotent-Replayed: true. A request that failed doesn't keep its key, so fix it and retry with the same one. - Same key, different body:
422 idempotency_key_reused(CStarValidationError). Use a new key for a new request. - First request still running:
409 idempotency_key_in_usewithRetry-Afterin seconds. With a key, the SDK waits that long and retries withinmaxRetries(0.18.1 and later); 0.18.0 throwsCStarConflictError. - Same key on another endpoint: also
409 idempotency_key_in_use, withoutRetry-After. - A request that never finishes (a crashed function) holds its key for up to 15
minutes, answering
409 idempotency_key_in_usemeanwhile. - Key not recorded:
503 service_unavailablewithRetry-After. Nothing ran; retry with the same key. - Key length: 1 to 250 characters. A longer key is a
400withparam: Idempotency-Key. - File uploads: multipart bodies are compared by their fields and each file's name, type and bytes, so a new form boundary on a retry still counts as the same request.
- Ignored: creating API keys, webhooks, webhook listeners, event stream tokens,
exports and billing portal sessions. A key does not make
webhooks.create,webhooks.listenorexport.createsafe to retry. From 0.18.2 the SDK doesn't send the key on those calls and doesn't retry a network error or 5xx. With 0.18.1 and earlier, leave the key off them (without one the SDK doesn't retry them), or use a client made withmaxRetries: 0. Checkwebhooks.list()before you retrywebhooks.create. A repeatedexport.createonly reads your data again (nothing is stored). A repeatedwebhooks.listenis a second CLI listener: remove it withwebhooks.unlisten(id), or it is swept after 5 minutes without a heartbeat. - Ticket messages keep their own receipt for the key: a retry returns the saved
message, and a different message with that key is a
409 idempotency_key_in_use.
Pick the key like a fingerprint
Make the key deterministic for the logical operation, not the network attempt. Some shapes that work:
- Form submissions:
submission_${formId}_${userId}. Same form, same user, same ticket. - Webhook handlers:
stripe_${event.id}. Stripe sends the same event ID on every retry. - Queue jobs:
job_${jobId}. Survives worker restarts and at-least-once delivery. - Bad shape:
crypto.randomUUID(). A fresh UUID per attempt is the same as no key.
Webhook handler that survives retries
The classic case. Stripe (or any webhook source) retries delivery on non-2xx responses; the idempotency key keeps you safe even when your handler runs twice.
import express from 'express';
import { CStarClient } from '@cstar.help/js';
const cstar = new CStarClient({
apiKey: process.env.CSTAR_KEY,
teamId: process.env.CSTAR_TEAM_ID
});
app.post('/stripe-webhook', async (req, res) => {
const event = req.body;
await cstar.tickets.create(
{
title: `Stripe issue: ${event.type}`,
priority: 'high',
// Tags are stored lowercase with _ turned into -,
// so invoice.payment_failed is saved as invoice.payment-failed.
tags: ['stripe', event.type]
},
{ idempotencyKey: `stripe_${event.id}` }
);
res.status(200).end();
}); Matching tags later? Compare against the stored form: tags are lowercased, spaces and underscores become a dash, and each is cut to 50 characters.
No key, no dedup
Skip idempotencyKey and the SDK won't retry a create or update after a network error
or a 5xx, because the first attempt may already have written. Retrying it yourself without a key can
create a duplicate. That's fine for fire-and-forget calls but a footgun for anything that creates
a row your customer cares about.