What a server library for this API must do to be called one. App and web SDKs, which run
in browsers and apps with a client key and a user token, follow their own interface
instead. The Node.js library (@kirora/node) is the
reference implementation: when this document and that code disagree, the code is right
and this document is a bug.
Pick the reference first and have every other language follow its shape. The alternative — answering “what should this method be called” once per language — means somebody arbitrates naming forever.
What volunteers are given
| The contract | The API reference: every operation with an operationId, its fields and its responses |
| The error catalogue | Errors |
| The behaviour behind every endpoint | The guides |
| The shape | The Node.js reference library |
| This checklist | Below |
Naming
- One namespace per tag:
client.contacts,client.campaigns. - One method per
operationId, named by dropping the resource:listContacts→contacts.list(),sendCampaign→campaigns.send(). Adapt casing to the language’s conventions and nothing else. - Put the
operationIdin the method’s doc comment so the mapping back to the reference survives. operationIdis the stable identity across every SDK, the reference pages and the changelog. It does not change; neither should your method name.
The checklist
Configuration
- API key required at construction; fail loudly if absent.
- Base URL defaults to Kirora’s API host and can be overridden, so tests can
point at a mock server. It is the host alone: the library adds
/v1to each path, because its methods and types are written for that version. - Injectable HTTP client or transport, so callers can add proxies and tests can stub.
- Configurable timeout, with a sane default.
- Never log the API key, including in debug output and error messages.
Requests
-
Authorization: Bearer <key>on every request. - Never send an organization id. The key determines the organization.
- Path parameters URL-encoded.
- Unwrap the
dataenvelope so callers do not write.dataeverywhere. Keepmetareachable on list calls. -
204returns nothing rather than attempting to parse a body.
Pagination
- Expose a single-page call and an auto-paginating iterator; make the iterator the natural choice.
- Stop on
nextCursor === null, not on an empty page. - Stream items; never buffer the whole collection before yielding.
- Treat the cursor as opaque — never construct, decode or inspect one.
- No
pageoroffsetparameter anywhere in the surface.
Errors
- One error type carrying
code,message,status,detailsand a request id. -
codecovers every code in the catalogue. - An unrecognised code must not crash. New codes are additive; fall back to a generic error carrying the raw string.
- Keep raw
detailsreachable —VALIDATION_ERRORfield errors live there. - Distinguish transport failures from API errors; a connection reset has no
code. - Never branch on
message.
Retries
- Retry 429 always: the server refused it before doing anything.
- Retry 5xx and transport failures only for a request safe to repeat:
GET,PUT,DELETE, any request carrying anIdempotency-Key, and the writes the idempotency guide lists as safe. A create without a key may have happened before the failure, and repeating it makes a second one. - Retry nothing else.
- Honour
Retry-Afterwhen present: never retry sooner (jitter adds to it, never takes from it), and surface the error rather than wait past the delay ceiling. Exponential backoff when it is absent. - Jitter every wait. Workers limited in the same second otherwise wake in the same second.
- Bounded attempts, configurable, disable-able.
- Surface the final failure; never fail silently or retry forever.
Idempotency
- Accept a caller-supplied
Idempotency-Keyon transactional send, push send, event tracking and campaign triggers. The API requires one on all four. - Never generate a key inside the retry loop. The key must be fixed before the first attempt or it protects nothing.
- Document which operations are safe to retry — see Idempotency.
Partner tenants
- Let a partner key act in a tenant: a client-wide
workspaceand a per-call override, sent as theKirora-Workspaceheader (the tenant’sexternalId).
Webhooks
- Ship
verifyWebhookSignature(secret, rawBody, signature, timestamp). - HMAC-SHA256 over
${timestamp}.${raw body}, compared in constant time, returning false rather than throwing on a length mismatch. Reject a timestamp outside the tolerance window, and accept any of the comma-separated signatures sent while a secret rotation overlaps. - Document the raw-body requirement prominently — framework body parsing is the usual reason verification fails in the field.
- Expose a helper to read the
X-Webhook-*headers, including the event id used for deduplication. - Keep the event type open; a
*subscription receives types added later.
User tokens
- Ship
createUserToken(tokenSecret, userId, { expiresInSeconds }): the token a customer’s server hands Kirora’s app and web SDKs at sign-in. HS256 over{ sub, iat, exp }, defaulting to one hour. It needs no API key and makes no call. - Check nothing the server checks: Kirora refuses a token valid for more than 24 hours, and that rule stays there.
Forward compatibility
- Deserialize permissively; unknown response fields are ignored, never fatal.
- Treat every enum as open.
- Take API changes deliberately: read the changelog, then update.
Proving it
The reference library’s conformance tests exercise it against these claims: the error catalogue matches the API’s, pagination stops on the right signal, 429 is retried and 400 is not, retries are bounded, and the webhook verifier agrees with the server’s own signer byte for byte, and a signed user token is accepted by Kirora. Port those tests along with the shape — an SDK that has not demonstrated the retry and webhook behaviour has not demonstrated the two things most likely to be wrong.
Scope of the reference
The reference implements the full cross-cutting machinery, and demonstrates the resource
pattern on the operations that exercise each distinct behaviour: cursor pagination,
identity upsert, state-based idempotency, and key-based idempotency. The remaining tags
are mechanically identical — one line per operation against client.request — and
deliberately left to the SDK authors rather than pre-empted here.
Request and response types are named models. A new namespace types its methods with them, not open objects.