Idempotency and rate limits

Two things separate an integration that works in a demo from one that works at 3 am under load: it retries safely, and it stays under limits it knows about. Both are simple once the rules are explicit.

Retrying safely

Most Talkif endpoints are naturally safe to retry: a GET is harmless, a PUT sets the same state twice, a DELETE of something already gone succeeds. The one that isn’t is POST /calls — retried blindly after a timeout, it can dial the same person twice.

Talkif doesn’t currently accept a client-supplied idempotency key on call creation. Use the request’s own identity instead:

1

Attach the contact

Pass contactId on POST /calls whenever you have one. It makes the call findable by contact, and it means a duplicate would be a second call to the same contact record — easy to spot.

2

On timeout, look before you retry

A timeout means you don’t know whether the call was created. Before re-issuing, list recent calls — GET /calls filtered by contactId (or phoneNumber) with startDate a few minutes back. A call created since your request → done. Nothing → retry.

3

Keep the SDK's retries for the rest

The official SDKs retry 408, 429 and 5xx twice with backoff. That’s right for every endpoint except call creation; for POST /calls, pass maxRetries: 0 and do the check above.

Campaigns are idempotent by construction: adding a contact who’s already in the campaign doesn’t add them twice, and a campaign never redials a contact whose call was answered.

What’s rate-limited

LimitDefaultApplies toOn breach
Concurrent calls10all calls in flight for the account — inbound, outbound, browseroutbound calls queue (HTTP 202) instead of failing; queue depth 100, then queue_full
Call placements per minute60POST /calls, campaign and schedule dials combinedrate_limit_exceeded — wait and retry
Assistant prompts10 / minutedashboard assistantwait
Authentication endpointssmall bursts (e.g. 5 login attempts, then 20 per 15 min)per IP429

Defaults are per account and can be raised — contact support with your expected peak concurrency. The account’s current values are shown in the dashboard’s account settings, and Limits explains how the queue prioritises when you’re at the ceiling.

A limited request gets a 429 (or a 4xx with the error code above) and a JSON error body:

{
"code": "rate_limit",
"title": "Rate Limit Exceeded",
"status": 429,
"detail": "Call rate limit exceeded",
"requestId": "550e8400-e29b-41d4-a716-446655440000"
}

Every API error has this shape — code is the field to branch on, requestId is what to quote to support. The full catalogue is on Error codes.

Back off exponentially from the first 429, jitter the delay, and never retry in a tight loop — the limiter counts attempts, so a hot retry loop keeps you limited longer.

Designing for the limits

  • Batch through campaigns, not POST /calls loops. A campaign paces itself to your concurrency and windows, retries no-answers, and skips DNC — a loop of POST /calls has to reimplement all of that and will hit the per-minute limit at 60.
  • Treat 202 as success. A queued call will be placed; read queueId and position and move on. Only 4xx/5xx need handling.
  • Use the event stream, not polling. Real-time events removes almost all read traffic from an integration.
  • One WebSocket per service instance, re-subscribing on reconnect — not one per call. The per-account connection cap is 20.
  • Set concurrency to what your own systems can absorb. If every call creates a ticket in your helpdesk, 50 concurrent calls create tickets at a rate your helpdesk’s API may not like.

Next