v1 · liveBase URL https://backend.talkstry.com/v1

Integrate TalksTry into your own systems.

Send order updates, alerts and replies from your backend over the same network your customers already chat on. This page is the contract: the flow end to end, the calls that make it up, and what comes back when something goes wrong.

Getting access

Organisation API access

For a business sending to its customers. Reviewed by our team, then any scope: OTP, single sends, broadcast, templates and member management, in sandbox and live.

Apply for API access →

Developer sandbox

For a personal account experimenting. Self-serve, no review, and permanently limited to otp:send, otp:verify and messages:send at 20 requests a minute and 500 a day. It can never hold broadcast, template or member scopes — not as a setting, but as a property of the credential. Its messages are real messages, so it can only send to your own number and to numbers whose owners confirmed a code you sent them.

Integration flow

  1. 1

    Register the organization

    Submit the business form and get verified.

    An organization account is created from the business registration form. Verification is manual — an admin approves the organization before any credentials exist, so this step is done once and never from code.

  2. 2

    Create API credentials

    Client ID + secret, from the console's API keys page.

    An approved organisation gets a sandbox key immediately; live keys are created in the console. The secret is shown exactly once, at creation, and stored only as a SHA-256 hash — a lost secret is rotated, never recovered. Create one credential per environment: a leaked staging key should never be able to message real customers.

  3. 3

    Exchange credentials for an access token

    POST /auth/token — client credentials grant.

    Tokens are short-lived (1 hour). Cache the token and reuse it until it expires; requesting a fresh token per API call will hit the auth rate limit long before the messaging one.

  4. 4

    Call the API

    Send messages, read threads, manage contacts.

    Every request carries the bearer token and an Idempotency-Key. Retrying with the same key returns the original result instead of sending a second message — the difference between a network blip and a customer receiving your notification twice.

  5. 5

    Receive webhooks

    Delivery and read receipts, pushed to you.

    Outcomes are asynchronous: sent, delivered and read events arrive at your HTTPS endpoint, signed with your webhook secret. Verify the signature, respond 2xx quickly, and do the real work off the request — we wait 10 seconds and then treat the attempt as failed. A failure is retried 5 times over about 15 minutes; a 4xx that says the request itself is wrong is not retried at all. Delivery is at-least-once: if your 2xx never reaches us we send again, so key your processing on the event id.

  6. 6

    Go live

    Swap the sandbox key for a production one.

    Nothing about the request shape changes when you switch — only the credential does. Both environments deliver real messages to real people; the difference is what the credential may do and who it may reach. An approved organisation's key can message its customers; a personal sandbox key can only reach numbers on its verified test list.

Authentication

Credentials are created on the API keys page and exchanged for a one-hour access token. The secret never travels on a normal API call — only on this one.

Request an access token
curl -X POST https://backend.talkstry.com/v1/auth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "live_8f3c1d9a4b2e",
    "client_secret": "••••••••"
  }'
200 OK
{
  "access_token": "eyJhbGciOiJIUzI1NiIs…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "otp:send otp:verify messages:send broadcast:send",
  "environment": "live"
}

The scope in that response is what the key may actually do. Scopes are fixed when a key is created — a call outside them returns 403 not_permitted.

  • otp:sendSend a one-time code to a number
  • otp:verifyCheck a code the recipient typed back
  • messages:sendSend a single message, and read its receipts
  • broadcast:sendMessage every member of your organisation
  • templates:manageRegister and edit message templates
  • members:manageManage the accounts your organisation owns

Sending a message

A send is accepted, not delivered: the call returns as soon as the message is queued, and the outcome arrives on your webhook. TheIdempotency-Keyis what makes a retry safe — repeat it and you get the original message back rather than a second one.

The recipient needs a TalksTry account. Messages are delivered inside TalksTry, not over SMS — a number with no account returns 404 not_found and nothing is sent.

Send a text message
curl -X POST https://backend.talkstry.com/v1/messages/send \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Idempotency-Key: order-10432-shipped" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+919876543210",
    "text": "Your order #10432 has shipped."
  }'
202 Accepted
{
  "id": "6ab357834ec807348eb477eb",
  "status": "queued",
  "to": "+919876543210",
  "created_at": "2026-09-22T09:14:02.511Z"
}

// Retrying with the SAME key and the SAME text returns the original:
// { "id": "...", "status": "duplicate", ... }  — nothing new was sent.
// The SAME key with DIFFERENT text is a 409 idempotency_conflict.

Webhooks

Every event is signed with your webhook secret. Verify the signature before trusting the body — the endpoint is public, so anyone can post to it.

Delivery is at-least-once. If your 2xx is lost on the way back — a timeout, a dropped connection — we cannot tell that from your server being down, so we send the same event again. Every request carries the same Talkstry-Event-Id, and for a receipt that id is derived from the message, so it is stable across retries. Record the ids you have processed and ignore a repeat.

Event payload
{
  "id": "evt_8b43159ce6615eded7886624",
  "type": "message.delivered",
  "created_at": "2026-09-22T09:14:05.002Z",
  "data": {
    "message_id": "6ab3a746b25688d30d5c875a",
    "status": "delivered",
    "occurred_at": "2026-09-22T09:14:04.881Z"
  }
}
Verify and acknowledge
// Express. The signature covers the RAW body, so the JSON parser must not
// have touched it yet.
app.post(
  "/webhooks/talkstry",
  express.raw({ type: "application/json" }),
  (req, res) => {
    // Talkstry-Signature: t=<unix seconds>,v1=<hex hmac>
    const header = req.header("Talkstry-Signature") || "";
    const parts = Object.fromEntries(
      header.split(",").map((part) => part.split("=")),
    );

    // The timestamp is part of what was signed, so an old capture cannot be
    // replayed. Reject anything outside your tolerance BEFORE comparing.
    const age = Math.abs(Date.now() / 1000 - Number(parts.t));
    if (!Number.isFinite(age) || age > 300) return res.sendStatus(401);

    const expected = crypto
      .createHmac("sha256", process.env.TALKSTRY_WEBHOOK_SECRET)
      .update(`${parts.t}.${req.body}`)
      .digest("hex");

    // timingSafeEqual throws when the lengths differ, so check that first.
    const given = Buffer.from(parts.v1 || "", "utf8");
    const mine = Buffer.from(expected, "utf8");
    if (given.length !== mine.length || !crypto.timingSafeEqual(given, mine)) {
      return res.sendStatus(401);
    }

    // Acknowledge first, work afterwards: we time out at 10s and retry.
    res.sendStatus(200);
    queue.add("talkstry-event", JSON.parse(req.body.toString()));
  },
);

Message templates

A template is a body a reviewer has approved. Register one in the console, and once it is approved send it by name with its variables filled in — the body comes from the approved record, so what goes out is what was reviewed.

Placeholders are {{1}}, {{2}} … numbered from 1 with no gaps, and variables fills them in order. Sending a template that is still under review returns 403 template_not_approved, and editing an approved template sends it back for review.

Send an approved template
curl -X POST https://backend.talkstry.com/v1/messages/send \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Idempotency-Key: order-10432-shipped" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+919876543210",
    "template": {
      "name": "order_shipped",
      "variables": ["#10432", "Friday"]
    }
  }'

Filling the variables

variables is a flat array, matched to the placeholders by position: the first value fills {{1}}, the second {{2}}, and so on. There are no names to match — only order.

How a call becomes a message
Approved body
  Hi {{1}}, your order {{2}} ships on {{3}}.

Your call
  "variables": ["Asha", "#10432", "Friday"]
                  |         |         |
                {{1}}     {{2}}     {{3}}

What the recipient reads
  Hi Asha, your order #10432 ships on Friday.
RuleWhat happens if you break it
Pass exactly as many values as the body has placeholders400 invalid_request — the message names the count it wanted and the count it got
Values must be strings or numbers400 invalid_request — objects, arrays, null and true are refused
At most 20 values400 from validation, before the template is even looked up
The template must be approved403 template_not_approved

Two details worth knowing, because both surprise people:

  • A placeholder repeated in the body is still one variable. Hi {{1}}, see you {{2}}, {{1}}! takes two values, and the first appears twice.
  • Your values are never re-scanned. A value that itself contains {{2}} is delivered as that literal text — it cannot be made to stand in for another slot.
A template with no variables
# A body with no placeholders takes no variables.
# Sending an empty array is fine; sending values is not.

curl -X POST https://backend.talkstry.com/v1/messages/send \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+919876543210",
    "template": { "name": "store_closed_today" }
  }'

Addressing by name or by id

Use name or id — whichever your integration already stores — but never both in one call, since the two could name different templates and we will not guess which you meant.

Send by template id
# Address a template by id instead of by name —
# useful when your database stores the id we returned.

curl -X POST https://backend.talkstry.com/v1/messages/send \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+919876543210",
    "template": {
      "id": "66f0a1c4e9b21d4a7c3f8e12",
      "variables": ["Asha", "#10432", "Friday"]
    }
  }'

Switching one off

A template can be deactivated in the console without being deleted. Sending it then returns 403 template_disabled, and it stops being listed as an option on /otp/send. Its approval survives, so reactivating it needs no second review — which is the point: a seasonal notice can be parked and brought back without queueing again.

Templates for one-time codes

An organisation must send one-time codes through an approved template. A bare POST /otp/send returns 400 template_required, and the response lists your approved authentication templates in data.availableTemplates. A personal sandbox key is exempt and gets the default wording.

An OTP template is stricter than a normal one. It must sit in the authentication category, and it must have exactly one placeholder — the code goes there. A marketing template is refused, because a code must not inherit a review that was granted for something else.

Send a code through your own wording
# Approved body (category: authentication)
#   {{1}} is your Acme login code. It expires in 5 minutes.
#
# You do NOT pass variables here — the code fills {{1}} itself.

curl -X POST https://backend.talkstry.com/v1/otp/send \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "+919876543210",
    "template": { "name": "login_code" }
  }'

{
  "status": "sent",
  "template": "login_code",
  "expires_in": 300
}

Note that /otp/send takes no variables: we generate the code and fill the single slot ourselves, so there is nothing left for you to pass. The code is never returned to you — only the recipient sees it.

Free-form text still works for /messages/send — the template requirement applies to one-time codes only. Editing an approved template sends it back for review, so an approved OTP notice cannot quietly become a marketing blast.

Delivery receipts

A message moves through sent → delivered → read. Poll it, or subscribe to message.delivered and message.read and let us tell you — webhooks are cheaper for both of us than a polling loop.

Fetch a message
curl https://backend.talkstry.com/v1/messages/6ab3a746b25688d30d5c875a \
  -H "Authorization: Bearer $ACCESS_TOKEN"

{
  "id": "6ab3a746b25688d30d5c875a",
  "status": "read",
  "created_at": "2026-09-22T09:13:58.004Z",
  "delivered_at": "2026-09-22T09:14:04.881Z",
  "read_at": "2026-09-22T09:15:22.117Z"
}

A message whose recipient has blocked the sender comes back with undeliverable_reason rather than sitting at sent forever.

The personal sandbox

Any personal TalksTry account can turn on a developer sandbox from Settings, with no review: OTP and single-message send, 20 requests a minute and 500 a day. Broadcast, templates and member management are never available to it.

A sandbox message is a real message, so a sandbox key can only send to your own number and to numbers whose owners have confirmed a code you sent them. Anything else returns 403 sandbox_recipient_not_allowed. When you need to reach your own customers, apply for API access.

API reference

Every endpoint, with a runnable sample in your language.

POST/auth/token

Create an access token

Exchanges a client id and secret for a bearer token valid for one hour. This is the only call that carries your secret — cache the token and reuse it until it expires rather than requesting one per API call.

Body

grant_typestringrequired
Always "client_credentials".
client_idstringrequired
From the API keys page. Its prefix is the environment: live_… or sandbox_…
client_secretstringrequired
Shown once, at key creation. Stored only as a hash — a lost secret is rotated, never recovered.

Response · 200

access_tokenstring
Send as Authorization: Bearer on every other call.
token_typestring
Always "Bearer".
expires_innumber
Seconds until the token stops working. Currently 3600.
scopestring
Space-separated scopes this key holds. Fixed when the key was created.
environmentstring
"live" or "sandbox" — the key's environment, not a request option.

Errors

  • 401invalid_client — The client id or secret is wrong, or the key was revoked. The same message is returned for both, so this endpoint cannot be used to discover which client ids exist.
  • 429rate_limited — More than 30 token requests a minute from one IP.
POST/auth/token
curl -X POST https://backend.talkstry.com/v1/auth/token \
  -H "Content-Type: application/json" \
  -d '{
  "grant_type": "client_credentials",
  "client_id": "live_8f3c1d9a4b2e",
  "client_secret": "sk_a7f2c9e1b4d68350a1c2"
}'
Response · 200
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "otp:send otp:verify messages:send broadcast:send",
  "environment": "live"
}
GET/organization

Identify the credential

Returns who the calling key belongs to and what it may do. Useful as a health check at boot: if this returns 200 with the scopes you expect, your credentials and environment are wired correctly.

Response · 200

idstring
The account this key acts as.
namestring
Organisation name, or the account holder's name for a sandbox key.
emailstring
Account email.
typestring
"organization" or "sandbox_user".
environmentstring
"live" or "sandbox".
scopesarray
What this key may do.
sandboxobject
Present only for a sandbox key: its status and rate limits.

Errors

  • 401invalid_token — The token is missing, expired or its key was revoked.
GET/organization
curl -X GET https://backend.talkstry.com/v1/organization \
  -H "Authorization: Bearer $ACCESS_TOKEN"
Response · 200
{
  "id": "6a7338745946e15320ae0988",
  "name": "Acme Logistics",
  "email": "ops@acme.example",
  "type": "organization",
  "environment": "live",
  "scopes": [
    "otp:send",
    "otp:verify",
    "messages:send",
    "broadcast:send"
  ],
  "sandbox": null
}
POST/messages/sendmessages:send

Send a message

Queues a message to one recipient and returns immediately — a 202 means accepted, not delivered. The outcome arrives on your webhook, or you can poll the message. Send either free-form text or an approved template, never both.

Headers

Idempotency-Keystringrequired
A unique id for this send.Repeat it and you get the original message back instead of a second one. Derive it from the business event (order-10432-shipped), never from a timestamp — a timestamp changes on retry, which is exactly when you need it not to.

Body

tostringrequired
Recipient in E.164 form.The number must already have a TalksTry account — messages are delivered inside TalksTry, not over SMS.
textstringoptional
The message body, up to 4000 characters. Required unless you send a template.
templateobjectoptional
An approved template to send instead of text.
template.namestringoptional
The template's registered name. Send this or template.id, not both.
template.idstringoptional
The id returned when the template was created. Same thing by a different handle — use whichever your integration already stores.
template.variablesarrayoptional
Strings or numbers filling {{1}}, {{2}} … by position — there are no names, only order. At most 20.The count must equal the number of DISTINCT placeholders in the approved body: a placeholder used twice is still one variable. Your values are never re-scanned, so a value containing {{2}} is delivered as that literal text.

Response · 202

idstring
The message id. Use it to poll status, and it appears in every webhook about this message.
statusstring
"queued" for a new send; "duplicate" when this Idempotency-Key was already used with the same body, meaning nothing new was sent.
tostring
Echo of the recipient.
created_atstring
When the message was accepted.

Errors

  • 400invalid_request — A field is missing or the wrong type — including an Idempotency-Key header that was not sent, a variable count that does not match the template, or a variable that is not a string or number.
  • 403template_disabled — The template exists and is approved, but the organisation has switched it off in the console.
  • 403template_not_approved — The named template is still under review, or was rejected.
  • 403sandbox_recipient_not_allowed — A sandbox key addressed a number that is not on its verified test list.
  • 404not_found — No TalksTry account is registered for that number.
  • 409idempotency_conflict — The same Idempotency-Key was used earlier with DIFFERENT content. Nothing is sent — use a new key.
  • 429rate_limited — More than 600 sends a minute on this key.
  • A 202 is not a delivery. Subscribe to message.delivered and message.read, or poll GET /messages/{id}.
  • A duplicate raises no second webhook and is not counted against your allowance — nothing happened twice.
  • Send template.name or template.id, never both: the two could name different templates and we will not guess which you meant.
POST/messages/send
curl -X POST https://backend.talkstry.com/v1/messages/send \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Idempotency-Key: order-10432-shipped" \
  -H "Content-Type: application/json" \
  -d '{
  "to": "+919876543210",
  "text": "Your order #10432 has shipped and arrives Friday."
}'
Response · 202
{
  "id": "6ab3a746b25688d30d5c875a",
  "status": "queued",
  "to": "+919876543210",
  "created_at": "2026-09-22T09:14:02.511Z"
}
GET/messages/{id}messages:send

Retrieve a message

The current status and receipts for a message you sent. Polling works, but webhooks are cheaper for both of us — use this to reconcile, not as your primary loop.

Path & query

idstringrequired
The id returned when the message was accepted.

Response · 200

idstring
The message id.
statusstring
"sent", "delivered", "read" or "failed".
tostring
Recipient account id.
created_atstring
When it was accepted.
delivered_atstring
When it reached the recipient's device. null until then.
read_atstring
When the recipient opened it. null until then.
undeliverable_reasonstring
Present only when the message can never be delivered — for example the recipient has blocked the sender.

Errors

  • 404not_found — No message with that id was sent from this account.
GET/messages/{id}
curl -X GET https://backend.talkstry.com/v1/messages/6ab3a746b25688d30d5c875a \
  -H "Authorization: Bearer $ACCESS_TOKEN"
Response · 200
{
  "id": "6ab3a746b25688d30d5c875a",
  "status": "read",
  "to": "699bdf1868fc992a64b2032d",
  "created_at": "2026-09-22T09:13:58.004Z",
  "delivered_at": "2026-09-22T09:14:04.881Z",
  "read_at": "2026-09-22T09:15:22.117Z"
}
POST/otp/sendotp:send

Send a one-time code

Generates a code and delivers it to the recipient as an ordinary TalksTry message from your account. The code is never returned to you — you verify it by passing back what the user typed. An organisation MUST name one of its approved authentication templates; a personal sandbox key may omit it and gets our default wording.

Body

phonestringrequired
Recipient in E.164 form. Must have a TalksTry account.
templateobjectrequired
The approved template to send the code in. Required for an organisation credential; optional for a personal sandbox key, which has no templates.It must be in the authentication category and have exactly one placeholder, {{1}}, which the code fills. A marketing template is refused: a code must not inherit a review granted for something else.
template.namestringoptional
The template's registered name. Send this or template.id, not both.
template.idstringoptional
The template's id, as an alternative to its name.

Response · 202

otpEventIdstring
Identifies this challenge. Keep it if you want to correlate with your own records.
channelstring
Always "in_app" — codes go as TalksTry messages, not SMS.
statusstring
"sent".
templatestring
The template the code went out in. Absent when the default wording was used.

Errors

  • 400template_required — An organisation credential sent no template. The response lists your approved authentication templates in `data.availableTemplates`.
  • 400invalid_request — The named template does not have exactly one placeholder — there is nowhere for the code to go, or nothing to fill a second slot with.
  • 403not_permitted — The named template is not in the authentication category.
  • 403template_disabled — The template exists and is approved, but the organisation has switched it off in the console.
  • 403template_not_approved — The named template is still under review, or was rejected.
  • 404not_found — No TalksTry account for that number, or no such template on this account.
  • 429rate_limited — One code per number per minute. The response carries Retry-After and retry_after.
  • The code expires in five minutes and can be used once.
  • The template requirement is the same idea as review itself: a business sending codes to its customers has its wording read first. A personal sandbox key is exempt — it can only reach numbers whose owners confirmed a code, so there is nobody to protect from it.
POST/otp/send
curl -X POST https://backend.talkstry.com/v1/otp/send \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "phone": "+919876543210",
  "template": {
    "name": "login_code"
  }
}'
Response · 202
{
  "otpEventId": "6ab39addff03a25a02910704",
  "channel": "in_app",
  "status": "sent",
  "template": "login_code"
}
POST/otp/verifyotp:verify

Verify a one-time code

Checks the code the recipient typed back to you. A code is single-use: verifying it consumes it, so a second attempt with the same code fails even though it was correct.

Body

phonestringrequired
The same number the code was sent to.
codestringrequired
4 to 8 digits, as the recipient typed it.

Response · 200

verifiedboolean
true only when the code was correct and unused.
statusstring
"verified" or "invalid".
messagestring
Human-readable outcome.

Errors

  • 400invalid_request — Wrong code, expired code, or a code already used. The body says which.
POST/otp/verify
curl -X POST https://backend.talkstry.com/v1/otp/verify \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "phone": "+919876543210",
  "code": "482913"
}'
Response · 200
{
  "verified": true,
  "status": "verified",
  "message": "Code verified"
}
POST/organizations/broadcastbroadcast:send

Broadcast to your organisation

Sends one message to every member account your organisation owns. Each member gets a real one-to-one conversation, not a shared thread, so replies come back to you individually.

Headers

Idempotency-Keystringrequired
A unique id for this broadcast.Applied per member, so a retry cannot double-send to anyone.

Body

textstringrequired
The message body, up to 4000 characters.

Response · 202

broadcastIdstring
Your Idempotency-Key, echoed.
recipientsnumber
How many members were addressed.
queuednumber
How many were accepted.
failednumber
How many could not be queued.
statusstring
"queued".

Errors

  • 403not_permitted — The key lacks broadcast:send — a sandbox key can never hold it.
  • 404not_found — The organisation has no members to broadcast to.
  • 429rate_limited — More than 30 broadcasts a minute.
POST/organizations/broadcast
curl -X POST https://backend.talkstry.com/v1/organizations/broadcast \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Idempotency-Key: notice-maintenance-2026-09" \
  -H "Content-Type: application/json" \
  -d '{
  "text": "Office closed on Monday for maintenance."
}'
Response · 202
{
  "broadcastId": "notice-maintenance-2026-09",
  "recipients": 42,
  "queued": 42,
  "failed": 0,
  "status": "queued"
}

Errors

Every response carries a Talkstry-Request-Id header, successes included. Log it. When something needs explaining, quoting one identifies the exact call — a timestamp does not — and you can look it up yourself in the console under Logs, where it resolves however old it is.

StatusCodeMeaningWhat to do
400invalid_requestA field is missing or malformed.`message` names the first problem in plain words — fix that and retry.
401invalid_tokenToken missing, expired or revoked.Request a new token; do not retry the same one.
403not_permittedThe credential lacks that scope.A key's scopes are fixed at creation — create a new key with the scope you need.
403template_not_approvedThat template is still under review, or was rejected.Check its status in the console. Editing an approved template sends it back for review.
403sandbox_recipient_not_allowedA sandbox key may only reach its verified test numbers.Add the number in Settings → Developer sandbox, or apply for API access.
404not_foundNo such message or template — or the number you addressed has no TalksTry account.Messages are delivered inside TalksTry, so the recipient must already have an account.
409idempotency_conflictSame Idempotency-Key, different body.Use a new key for a genuinely new request.
429rate_limitedToo many requests in the window.Back off for `Retry-After` seconds.
5xxserver_errorOur side failed.Retry with exponential backoff and the same Idempotency-Key.

Rate limits

  • POST /auth/token

    30 requests / minute (per IP)

  • POST /messages/send

    600 requests / minute

  • GET /messages/{id}

    1,200 requests / minute

  • POST /otp/send

    300 requests / minute

  • POST /otp/verify

    600 requests / minute

  • POST /organizations/broadcast

    30 requests / minute

  • Any personal sandbox key

    20 / minute and 500 / day, whichever comes first

Every response carries X-RateLimit-Remaining. On a 429, wait the number of seconds in Retry-After — retrying sooner only pushes the window further out.