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.

StatusDescription
200OK: request succeeded.
201Created: a new resource was persisted.
204No Content: request succeeded with no response body (used by delete endpoints).
400Bad Request: a domain/balance precondition was not met, or a request-level guard rejected the input.
401Unauthorized: no Authorization header, an invalid key, or the key no longer exists.
403Forbidden: the API key lacks the required capability, is pinned to a different domain, or the project is suspended.
404Not Found: the resource does not exist in this project.
409Conflict: a uniqueness constraint, idempotency key, or scheduled email state prevents this operation.
422Unprocessable Entity: the request failed schema validation. The body carries a fields map.
429Too Many Requests: per-key or per-project rate limit hit.
500Internal 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": {
"from": ["Invalid email format. Use \"[email protected]\" or \"Name <[email protected]>\""],
"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.

Node.js
import {
Sevk,
SevkError,
ValidationError,
ForbiddenError,
RateLimitError
} from 'sevk'
const sevk = new Sevk(process.env.SEVK_API_KEY!)
try {
await sevk.emails.send({
from: 'sevk <[email protected]>',
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
}
}
Python
import time
from sevk import (
Sevk,
SevkError,
ValidationError,
ForbiddenError,
RateLimitError,
)
sevk = Sevk('sevk_xxxxxxxxx')
try:
sevk.emails.send({
'from': 'sevk <[email protected]>',
'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