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 (curl, Python, Go, PHP), see Send an email. For per-endpoint request and response detail, see the API reference.
Base URL
https://api.samva.dev/v1All endpoints are relative to this base URL.
Authentication
Every request authenticates with an API key passed in the X-API-Key header.
X-API-Key: samva_sk_live_your_api_keySee API keys for key format and management.
Headers
| Header | Required | Description |
|---|---|---|
X-API-Key | Yes | Your API key. |
Content-Type | For requests with a body | application/json for POST, PUT, and PATCH. |
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:
| Method | Path | Purpose |
|---|---|---|
POST | /v1/messages | Send a message. |
GET | /v1/messages | List 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"]
}
}Recipient fields
to is required. cc and bcc are optional. Each field is an array, and each entry is one of:
| Form | Example |
|---|---|
| Email address object | { "email": "ada@example.com" } |
| Existing contact reference | { "contactId": "c0a8011e-0000-4000-8000-000000000001" } |
Both forms resolve to the same POST /v1/messages endpoint.
email object
| Field | Type | Required | Description |
|---|---|---|---|
subject | string | Cond. | Email subject line. Required unless templateId is provided. |
html | string | Cond. | HTML body. Required unless templateId is provided. |
text | string | No | Plain-text body. |
replyTo | string[] | No | Reply-To address(es). |
templateId | string | No | Send from a stored template instead of inline subject/html. |
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": "3f2a9c1e-7b4d-4e8a-9c2f-1a2b3c4d5e6f",
"status": "pending",
"channel": "email",
"createdAt": "2024-01-15T10:30:00.000Z"
}See the API reference for the complete response schema.
Error
Errors are returned as a flat JSON object. A _tag field carries the machine-readable
discriminator, and the remaining fields are specific to that error type. There is no wrapper
object; the error fields sit at the top level of the response body.
A validation failure (422):
{
"_tag": "ValidationError",
"message": "Invalid request body",
"fields": {
"to": ["Must be a valid email address"]
}
}The _tag value maps to the HTTP status code:
_tag | Status | Fields |
|---|---|---|
ValidationError | 422 | message, fields (optional) |
ResourceNotFoundError | 404 | resource, id |
UnauthorizedError | 401 | message |
PaymentRequiredError | 402 | resource, currentUsage, limit |
ForbiddenError | 403 | message |
ConflictError | 409 | message, resource |
RateLimitedError | 429 | retryAfterSeconds |
ExternalServiceError | 502 | provider, message |
InternalError | 500 | message |
For example, a missing resource (404):
{
"_tag": "ResourceNotFoundError",
"resource": "Message",
"id": "3f2a9c1e-7b4d-4e8a-9c2f-1a2b3c4d5e6f"
}A rate-limit response (429):
{
"_tag": "RateLimitedError",
"retryAfterSeconds": 30
}For the full list of error types, their fields, and how to resolve each one, see the Error reference. The machine-readable source of truth is the OpenAPI specification.
HTTP status codes
| Code | Meaning | Notes |
|---|---|---|
200 | OK | Request completed. |
201 | Created | Resource created (a successful send returns 201). |
400 | Bad Request | Malformed request. |
401 | Unauthorized | Missing or invalid API key. |
402 | Payment Required | Usage limit or plan restriction. |
403 | Forbidden | Key lacks permission for the resource. |
404 | Not Found | Resource does not exist. |
409 | Conflict | Duplicate request. |
422 | Unprocessable Entity | Validation failed. |
429 | Too Many Requests | Rate limit exceeded. |
500 | Internal Server Error | Unexpected server error. |
502 | Bad Gateway | Upstream provider error. |
503 | Service Unavailable | Temporary outage; retry later. |
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.
| Plan | Sustained rate | Burst | Burst per API key |
|---|---|---|---|
| Free | 5 / second | 10 | 5 |
| Starter | 10 / second | 20 | 10 |
| Growth | 25 / second | 50 | 25 |
| Scale | 50 / second | 100 | 50 |
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.
Campaign and scheduled sends draw on the same buckets as direct API sends, and pace themselves against the rate limit rather than failing.
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:
{
"_tag": "RateLimitedError",
"operation": "MessageService.send",
"retryAfterSeconds": 1
}Two response headers accompany it:
| Header | Meaning |
|---|---|
Retry-After | Whole seconds to wait, matching retryAfterSeconds. |
X-Samva-Throttle-Source | samva_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.
| Plan | Emails per day |
|---|---|
| Free | 1,000 |
| Starter | 5,000 |
| Growth | 20,000 |
| Scale | 100,000 |
Two caps sit under those numbers and apply whichever is lower:
| Condition | Emails per day |
|---|---|
| No payment method on file | 100 |
| First 24 hours after the workspace is made | 200 |
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 body
and no Retry-After, because waiting a few seconds is not what clears it:
{
"_tag": "PaymentRequiredError",
"operation": "MessageService.send",
"resource": "daily send quota",
"currentUsage": 1000,
"limit": 1000
}Add a payment method, move to a plan with a higher quota, or wait for the UTC day to turn over.
Paused sending
Samva pauses a workspace'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 a workspace is paused, every send returns 403 Forbidden with a ForbiddenError whose
message names the observed rate. No amount of waiting or upgrading clears it; email
support@samva.dev to have the pause reviewed.
Pagination
List endpoints accept page and limit query parameters.
| Parameter | Default | Description |
|---|---|---|
page | 1 | Page number (1-based). |
limit | 20 | Items per page (maximum 100). |
GET /v1/messages?page=2&limit=50Paginated responses include a pagination object.
{
"data": [],
"pagination": {
"page": 2,
"limit": 50,
"total": 245,
"totalPages": 5
}
}| Field | Type | Description |
|---|---|---|
pagination.page | number | Current page. |
pagination.limit | number | Items per page. |
pagination.total | number | Total matching items. |
pagination.totalPages | number | Total number of pages. |
Idempotency
POST /v1/messages accepts an optional idempotencyKey in the JSON body. 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.
Repeating an identical request with the same key returns the original message without creating another message, provider send, 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.