Errors
API failures return a JSON error body and a non-2xx HTTP status.
Error shape
Structured API errors use this envelope. statusCode mirrors the HTTP status, code is a stable UPPER_SNAKE_CASE identifier, and message is a human-readable explanation.
{"statusCode": 404,"code": "NOT_FOUND","message": "Webhook not found"}
Framework-level failures may instead return { "error": string, "message": string }. In that case, branch on the HTTP status and treat the machine code as unavailable.
Schema validation failures from the Zod validator return HTTP 422 with the same flat shape plus a fields map of per-field messages:
{"message": "Validation failed","statusCode": 422,"code": "VALIDATION_ERROR","fields": {"to": ["Invalid email"],"from": ["Required"]}}
Status codes
Branch your retry logic on the status class (2xx / 4xx / 5xx) and, when needed, on the code field. The exact code set may grow over time; the classes will not.
| Status | Description |
|---|---|
200 | OK: request succeeded. |
201 | Created: a new resource was persisted. |
204 | No Content: request succeeded with no response body (used by delete endpoints). |
400 | Bad Request: a domain/balance precondition was not met, or a request-level guard rejected the input. |
401 | Unauthorized: no Authorization header, an invalid key, or the key no longer exists. |
403 | Forbidden: the API key lacks the required capability, is pinned to a different domain, or the project is suspended. |
404 | Not Found: the resource does not exist in this project. |
409 | Conflict: a uniqueness constraint, idempotency key, or scheduled email state prevents this operation. |
422 | Unprocessable Entity: the request failed schema validation. The body carries a fields map. |
429 | Too Many Requests: per-key or per-project rate limit hit. |
500 | Internal Server Error: unexpected server-side failure. Retried sends must reuse the original Idempotency-Key and request body. |
A timeout or server error does not prove that no email was queued. Supply an Idempotency-Key on the first send attempt, then reuse that key and the identical body when retrying with backoff. Do not switch to a new key to bypass a cached error response: the original attempt may already have created emails.
Error codes
The code field is the piece you branch on in code. These are the ones you will see in practice:
VALIDATION_ERROR
The request body failed the endpoint's Zod schema. The response is HTTP 422 with a flat body and a fields map of field name to the list of violated rules.
{"message": "Validation failed","statusCode": 422,"code": "VALIDATION_ERROR","fields": {"to": ["Invalid email"]}}
BAD_REQUEST
A domain or balance precondition was not met, or a request-level guard rejected the input (for example, an invalid webhook URL or unknown event name).
{"statusCode": 400,"code": "BAD_REQUEST","message": "Invalid webhook URL: loopback addresses are not allowed"}
UNAUTHORIZED
No Authorization header, a malformed bearer token, or a key that was revoked.
{"statusCode": 401,"code": "UNAUTHORIZED","message": "Invalid API key"}
FORBIDDEN
The key authenticated but the action was refused. Typical causes: the key is missing the capability for this endpoint, the key is pinned to a different domain than the from address, the project is suspended, or the sending domain is not verified on the project.
{"statusCode": 403,"code": "FORBIDDEN","message": "API key does not have required capability: email:send"}
NOT_FOUND
The resource does not exist in the project the API key is scoped to. sevk never leaks cross-project existence. A record owned by another project returns 404, not 403.
{"statusCode": 404,"code": "NOT_FOUND","message": "Webhook not found"}
CONFLICT
A resource in this project already matches a uniqueness constraint for the request (for example, a contact with the same email already exists in the audience).
{"statusCode": 409,"code": "CONFLICT","message": "Contact already exists"}
TOO_MANY_REQUESTS
The project or key exceeded its window. Most rate-limited endpoints include a Retry-After header with seconds until the next allowed request, plus X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset. The email send path's per-project limiter returns 429 without these headers; fall back to the seconds mentioned in the message or a fixed backoff.
{"statusCode": 429,"code": "TOO_MANY_REQUESTS","message": "Rate limit exceeded. Try again in 60 seconds."}
Handling errors
The SDKs throw a typed error hierarchy rooted at SevkError, which carries statusCode and code. Subclasses add the extras: ValidationError.fields and RateLimitError.retryAfter. Branch on the class or the code, not the message, which may change.
import {Sevk,SevkError,ValidationError,ForbiddenError,RateLimitError} from 'sevk'const sevk = new Sevk(process.env.SEVK_API_KEY!)try {await sevk.emails.send({subject: 'Hi',html: '<p>Hi</p>'})} catch (error) {if (error instanceof ValidationError) {console.error('Bad payload:', error.fields)} else if (error instanceof ForbiddenError) {// Verify the domain, check capability scopes, or remove the key pin.console.error(error.message)} else if (error instanceof RateLimitError) {await new Promise(r => setTimeout(r, (error.retryAfter ?? 1) * 1000))} else if (error instanceof SevkError) {// Branch on error.code (UPPER_SNAKE_CASE) for anything else.console.error(error.statusCode, error.code, error.message)throw error} else {throw error}}
import timefrom sevk import (Sevk,SevkError,ValidationError,ForbiddenError,RateLimitError,)sevk = Sevk('sevk_xxxxxxxxx')try:sevk.emails.send({'subject': 'Hi','html': '<p>Hi</p>',})except ValidationError as err:print('Bad payload:', err.fields)except ForbiddenError as err:# Verify the domain, check capability scopes, or remove the key pin.print(err.message)except RateLimitError as err:time.sleep(err.retry_after or 1)except SevkError as err:# Branch on err.code (UPPER_SNAKE_CASE) for anything else.print(err.status_code, err.code, err.message)raise