REST API

Reference REST conventions for Samva's email API, covering auth, request and response shapes, status codes, rate limits, pagination, and errors.

Conventions reference for Samva's REST API. Every email send and management call resolves to a request against this API. This page documents the shared conventions: base URL, headers, body and response shapes, status codes, rate limiting, pagination, idempotency, and error format.

For a runnable send walkthrough with the SDK and curl, see Send an email. For per-endpoint request and response detail, see the API reference.

Base URL

https://api.samva.dev/v1

All endpoints are relative to this base URL.

Versioning and deprecation

The first path segment is the API's major version. Samva may add backward-compatible endpoints, optional request fields, or response fields within /v1. A change that breaks an existing request or response contract requires a new major path such as /v2.

When an operation is deprecated, its OpenAPI operation is marked deprecated: true and the replacement is documented. If removal is scheduled, affected responses include Deprecation and Sunset headers, and the documentation publishes the removal date. No /v1 operation currently has a scheduled sunset.

Authentication

Server integrations authenticate with an API key in the X-API-Key header. The key determines the organization.

X-API-Key: samva_sk_live_your_api_key

A CLI OAuth session authenticates with Authorization: Bearer <token> instead and names the organization in x-org-slug. See Authentication for every credential type, key management, and permissions.

Headers

HeaderRequiredDescription
X-API-KeyOne credentialYour API key.
AuthorizationOne credentialBearer <token> for an OAuth session.
x-org-slugWith a CLI OAuth sessionThe organization the request acts on.
Content-TypeFor requests with a bodyapplication/json for POST, PUT, and PATCH.
Idempotency-KeyNoMakes a retried request safe. See Idempotency.

Content type

Requests carrying a body must send Content-Type: application/json. All responses are returned as JSON.

Endpoints

All operations are REST endpoints under the base URL. The send path is the most common:

MethodPathPurpose
POST/v1/messagesSend a message.
GET/v1/messagesList messages.
GET/v1/messages/{id}Retrieve a single message.

There is no email.send HTTP endpoint. The SDK's samva.email.send is an ergonomic wrapper that posts to POST /v1/messages. All REST calls use the POST /v1/messages body shape below.

Full per-resource endpoint listings (conversations, contacts, webhooks, API keys, organizations) live in the API reference.

Request body

POST /v1/messages accepts a JSON body with a channel discriminator and channel content nested under a matching key. For email, content is nested under email.

{
  "to": [{ "email": "ada@example.com" }],
  "cc": [{ "email": "grace@example.com" }],
  "channel": "email",
  "email": {
    "subject": "Welcome to Samva",
    "html": "<h1>Welcome!</h1>",
    "text": "Welcome!",
    "replyTo": ["support@example.com"]
  }
}

Request size

A request body is at most 6 MB. A larger body is refused with 413 and a { "message": "Request Entity Too Large" } body before the API reads it, so the response carries no _tag. JSON escaping counts toward the size: every " inside an HTML string adds a byte. Send files larger than the inline attachment limit as uploaded media; see Send an attachment.

Recipient fields

to is required. cc and bcc are optional. Each field is an array, and each entry is one of:

FormExample
Email address object{ "email": "ada@example.com" }
Existing contact reference{ "contactId": "contact_k9mvt4znw8rh7q2x" }

Both forms resolve to the same POST /v1/messages endpoint.

email object

The email object needs exactly one content source. Inline content uses subject and at least one of html or text. Template content uses exactly one of templateId or templateSlug and omits subject, html, and text. Every field of the object, including attachments, template data, threading, unsubscribe groups, and tracking, is documented on Send a message.

Response format

Success

A successful POST /v1/messages returns 201 with the created message object. A newly accepted send starts in pending status. The full object carries many more fields (recipients, deliveries, metadata); the most relevant are shown here:

{
  "id": "msg_7q2xk9mvt4znw8rh",
  "status": "pending",
  "channel": "email",
  "createdAt": "2024-01-15T10:30:00.000Z"
}

The send receipt and full message include skippedRecipients: an array of { address, reason } entries for addresses excluded by bounce, complaint, unsubscribe, or manual suppression. Group consent uses reason: "group-unsubscribe" and also includes unsubscribeGroupId. The array is empty when no address is excluded. If every address is excluded, the request returns 422 ValidationError with the addresses and reasons in fields.

An accepted message can wait. waitReason: "content-review" means the content needs review; reviewExpectedAt gives the expected decision time. waitReason: "screening-unavailable" means Samva's content check is temporarily unavailable. The check is retried automatically when the service recovers, and reviewExpectedAt is null. Read the message with messages.get to observe progress instead of submitting another send. Repeating the request with the same idempotency key returns its original skipped-recipient snapshot.

See the API reference for the complete response schema.

Error

An error is a flat JSON object whose _tag names the error and sets the HTTP status. Every error type, its status, its fields, and how to resolve it are in the Error reference, along with what to retry and the headers that name which layer refused.

Sending limits

Two limits govern sending, and they answer different questions. The rate limit bounds how fast you may send; the daily quota bounds how much you may send in a day. A request has to clear both.

Rate limiting

Send requests are admitted against a token bucket scoped to your organization. Your plan sets the sustained rate, and the bucket holds two seconds of that rate as burst, so a client that issues a second's work in one round of parallel requests is admitted in full.

PlanSustained rateBurstBurst per API key
Free5 / second105
Starter10 / second2010
Growth25 / second5025
Scale50 / second10050

All of your API keys draw from that one organization allowance, first-come. Adding keys does not add throughput, and Samva does not reserve a share of the sustained rate for each key: if one key sends continuously at the limit, your other keys will be throttled for as long as it does. Pace your own traffic if you need one integration to stay responsive while another runs a large job.

Each key does carry its own burst cap, at half the organization's burst. That is the one per-key bound: a single key cannot absorb your entire burst in one spike, so a second key always finds some headroom at the start. Both buckets have to admit a request, and a request that either one refuses draws down neither.

Scheduled sends draw on the same buckets as direct API sends, and pace themselves against the rate limit rather than failing. Campaign sends do not draw on the buckets: a campaign is paced by its own sending lane, which keeps it from holding back your direct email, and it still counts against the daily quota.

The rate limit counts requests, not accepted messages. A retry carrying an Idempotency-Key you have already used returns the original message without sending anything again, but it still draws on the bucket. Your plan's included message allowance is metered separately and is not charged for that replay.

When the rate limit is exceeded, the API returns 429 Too Many Requests with a RateLimitedError body. Two response headers accompany it:

HeaderMeaning
Retry-AfterWhole seconds to wait, matching retryAfterSeconds.
X-Samva-Throttle-Sourcesamva_admission when the plan limit refused the request.

Back off for Retry-After seconds and retry. Waiting the advertised interval is always enough for the same request to be admitted.

A 429 without X-Samva-Throttle-Source did not come from your plan limit. It was shed by the network edge or the API gateway ahead of the application, usually as a response to abnormal request volume. Retry with exponential backoff.

Daily send quota

Each plan also caps how many emails one organization may have accepted in a UTC day. Unlike the rate limit, this one does not refill until the day turns over.

PlanEmails per day
Free1,000
Starter5,000
Growth20,000
Scale100,000

An organization on the Free plan, or with no plan, is capped lower until it has a payment method on file. A paid plan always receives its own quota.

ConditionEmails per day
Free plan or no plan, with no payment method on file100

The daily quota counts accepted emails, not requests. A request the rate limit already refused does not draw on it, and neither does a request the quota itself refuses.

When the quota is spent, the API returns 402 Payment Required with a PaymentRequiredError whose resource is daily send quota, and no Retry-After, because waiting a few seconds is not what clears it.

Add a payment method, move to a plan with a higher quota, or wait for the UTC day to turn over.

Probation limits

A new organization sends on probation until it has a delivery record of its own. Probation limits contain a sender that mailbox providers have not seen yet; they are not part of your plan, and your plan's rate limit and daily quota still apply beside them. Whichever limit is lower is the one you meet first.

LimitWhile on probation
Recipients per UTC hour50
Recipients per UTC day200
Share of a day's recipients at one domain60%, once the day has more than 20 recipients

Probation limits count accepted recipients, not requests: an email to three recipients counts three. The domain share keeps a day's mail from going mostly to one mailbox provider or one company's domain, and it does not apply while the day's total is small, so test sends to your own inbox are not refused.

A send over a probation limit returns 429 Too Many Requests with a RateLimitedError, X-Samva-Throttle-Source samva_admission, and a Retry-After that runs to the start of the next UTC hour or day. Nothing is accepted or counted.

Probation ends when your organization graduates. See Probation for new organizations for what graduation takes and what else applies while you are on probation.

Paused sending

Samva pauses an organization's sending automatically when too much of its recent mail bounces or is reported as spam. See Deliverability for the thresholds and what to do about one.

While an organization is paused, every send returns 403 Forbidden with a ForbiddenError whose message names why. No amount of waiting or upgrading clears it; email support@samva.dev to have the pause reviewed. Mail accepted before the pause stays queued with the sending-paused wait reason, and scheduled sends and campaigns are held; they continue when the pause is released.

An organization that is disabled permanently returns the same 403 for every send, its API keys stop authenticating, and its scheduled sends and campaigns are cancelled.

Pagination

Every list endpoint that can grow without bound pages by cursor. It accepts limit and cursor, and answers items and nextCursor; pass nextCursor back unchanged as cursor to read the next page. nextCursor is null on the last page. Each list operation's page in the API reference publishes its order and its default and maximum limit.

A list that filters by time takes after and before, ISO 8601 instants that bound the time its API reference page names, usually creation time. On a list both are exclusive. Statistics, usage, and analytics reads take the same two names for a half-open range: after inclusive, before exclusive.

GET /v1/messages?limit=50&after=2026-10-01T00:00:00Z
GET /v1/messages?limit=50&cursor=eyJhdCI6IjIwMjYtMTAtMDFU...
{
  "items": [],
  "nextCursor": "eyJhdCI6IjIwMjYtMTAtMDFU..."
}

A cursor is opaque: do not build or edit one. A cursor that does not decode answers 422 ValidationError with a cursor entry in fields. A cursor page never counts the total, so a list stays as fast on its thousandth page as on its first.

A few lists are bounded by what an organization can hold, such as custom fields, unsubscribe groups, forwarding rules, and a message's attachments. They answer every entry as items in one response and take no cursor; their API reference page names the bound.

Paths and actions

A resource is addressed as /v1/<resource>/{id}. A state change that is not an edit of fields is its own action, POST /v1/<resource>/{id}/<action>: for example POST /v1/campaigns/{id}/archive, POST /v1/api-keys/{id}/revoke, and POST /v1/email/domains/{id}/verify.

Partial updates

PATCH changes the fields you name and keeps the rest. A collection on a resource, such as a contact's tags or a conversation's tags, categories, and channels, changes through its own POST (add) and DELETE (remove) endpoints, never through PATCH. metadata merges by key: a key you send replaces the stored value, a null value deletes the key, and a key you leave out stays.

Idempotency

POST /v1/messages accepts an optional Idempotency-Key header. Use a stable key for one logical send whenever your application may retry. The key is scoped to your organization and must contain 1 to 255 characters. Campaign schedule and scheduled-message create keep idempotencyKey in the JSON body. Those are different endpoints.

Repeating an identical request with the same key returns the original message without creating another message, delivery, or billing effect. Reusing the key with different request data returns 409 Conflict. If an attempt fails before the send is committed, Samva releases the claim so a corrected retry can proceed with the same key.

OpenAPI specification

The live machine-readable specification is published at api.samva.dev/v1/openapi.json. See the OpenAPI reference for details.

Related documentation

On this page