SDKs

Official clients. Both are hand-written, not generated.

package source
Node / TypeScript @sendoka/node packages/sdk-node
Python sendoka packages/sdk-python

Install

# Node / TypeScript — published on npm under the @sendoka scope
npm install @sendoka/node

The Python SDK is not published to PyPI yet. Do not install a package named sendoka from PyPI: Sendoka does not hold that name there, so whatever a registry serves under it is not ours — and it would run on the machine that holds your API key. Install it from a checkout of the repository instead:

pip install ./packages/sdk-python

Why not generated

The generated shape of this API is a flat bag of postV1EmailsBatch functions. The parts callers actually get wrong are the parts a generator has nothing to say about, and the clients enforce them:

One idempotency key per logical call, reused across every retry. A key minted per attempt is worse than no key at all — each retry looks like a fresh request and delivers a duplicate, which is the exact failure the retry was added to prevent. The key is minted outside the retry loop, automatically, on every endpoint that honours Idempotency-Key: emails.send, emails.sendBatch / send_batch, sms.send, sms.sendBatch / send_batch, audiences.send, verifications.create, and POST /v1/phone-numbers, /v1/brands, /v1/campaigns through the underlying client.

A write without a key is not resent through an ambiguous failure. A timeout, a dropped connection, a 408 or a 5xx does not say whether the server acted — the first attempt may already have sent the message. The clients retry through one only when a replay cannot act twice: a GET, or a write on one of the endpoints above. Any other POST, PATCH or DELETE is retried only on a 429, because the rate limiter refuses a request before anything runs. When one of those fails, find out what happened before sending it again.

409 is not retryable. It means an idempotency key is in flight or the body changed under one; hammering it makes both worse. Only 408, 429 and 5xx retry, and a server-sent Retry-After beats the exponential backoff — up to 60 seconds. A longer Retry-After is an hourly or daily ceiling, not a rate: the error goes straight to the caller with retryAfter / retry_after set rather than the call blocking for an hour. A 429 that is a quota rather than a rate (USAGE_LIMIT_EXCEEDED, TENANT_QUOTA_EXCEEDED, PLAN_RESOURCE_LIMIT, WARMUP_LIMIT_EXCEEDED, PROVIDER_QUOTA_EXCEEDED, SANDBOX_DAILY_LIMIT, and for test keys TEST_SCHEDULE_LIMIT_EXCEEDED) and a 500 AUDIENCE_SEND_INCOMPLETE are not retried at all.

The key comes back on the error. err.idempotencyKey (Node) / err.idempotency_key (Python), on both the API error and the connection error, is the key the request carried, including one the client minted. Make the same call again with that key and the server replays what the first attempt did instead of doing it twice. For retries that outlive one call — a queue redelivery, a rerun job — derive your own key from something stable about that one call, such as an order id, and pass it every time. Never key a verification by user or destination: a replay answers with the first code's verification, expired or not, and sends nothing.

A verification check is not retried. A 503 on verifications.check means the attempt could not be recorded, and the user's code is still good. The check takes no key, and a resent check whose first attempt did land would spend a second attempt or answer not_found for the code it had just approved, so the clients surface the 503 instead: ask the user to submit the code again.

Paging stops on the cursor, not just the flag. Both clients return when has_more is false or next_cursor is null. Trusting the flag alone is how hand-rolled paging becomes an infinite loop.

Webhook verification

Both ship verifyWebhookSignature / verify_webhook_signature, which verify X-Sendoka-Signature-V2 — HMAC over `${timestamp}.${body}`. The legacy X-Sendoka-Signature signs the body alone, so a captured delivery replays forever.

Three things they get right that a two-line implementation usually does not:

  • Raw body. A re-serialized object is a different string and every check fails in a way that looks like a wrong secret. Both READMEs name the framework-specific incantation.
  • Skew in both directions. A delivery timestamped in the future is as suspect as a stale one; a one-sided check is defeated by a clock the attacker controls.
  • Rotation lists. During a secret rotation the sender signs with both secrets and sends them comma-separated. Any match counts.

Comparison is constant-time, and a length-mismatched candidate returns false rather than throwing — Node's timingSafeEqual raises on differing lengths, which turns a bad signature into a 500.

Coverage

Emails, SMS (including MMS), audiences and contacts, verifications, inbound, suppressions, analytics, jobs. Anything not wrapped is reachable through the underlying client:

await sendoka.client.post("/v1/whatever", body);
sendoka.request("POST", "/v1/whatever", json=body)

The same retry policy applies there: a write made this way is resent through a timeout, network error, 408 or 5xx only when the endpoint honours Idempotency-Key. Otherwise it is retried on a 429 alone.