SDKs

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

package install
Node / TypeScript @sendoka/node npm install @sendoka/node
Python sendoka pip install sendoka

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 all three are enforced by the clients:

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.

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.

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)