API conventions
These conventions hold for every endpoint of the Anypost HTTP API: how requests are shaped, how responses and errors come back, and how pagination and idempotency work. Individual endpoint docs assume them.
Base URL
https://api.anypost.com/v1
Every request goes over HTTPS to a path under /v1. The version is part of
the path; v1 is the only version.
Code samples
Most examples in these docs carry a language picker: use the tabs above a code
block to switch between curl, the TypeScript SDK, the Python SDK, the PHP SDK,
the Ruby SDK, the Rust SDK, the Go SDK, the Java SDK, and the .NET SDK. Your
choice is remembered as you move between pages. The API
is plain JSON over HTTPS, so any HTTP client works.
An official TypeScript SDK is available for Node 18+, Bun, and Deno (source on GitHub):
npm install anypostimport { Anypost } from 'anypost';
const anypost = new Anypost('ap_your_api_key');
const { id } = await anypost.email.send({
from: '[email protected]',
to: ['[email protected]'],
subject: 'Hello from Anypost',
text: 'It worked.',
});The TypeScript SDK can also render a message body written in Markdown. See Send email as Markdown.
An official Python SDK is available for Python 3.9+, with sync and async clients (source on GitHub):
pip install anypostfrom anypost import Anypost
client = Anypost("ap_your_api_key")
result = client.email.send({
"from": "[email protected]",
"to": ["[email protected]"],
"subject": "Hello from Anypost",
"text": "It worked.",
})An official PHP SDK is available for PHP 8.1+ (source on GitHub):
composer require anypost/anypost-phpuse Anypost\Anypost;
$client = new Anypost("ap_your_api_key");
$email = $client->email->send([
"from" => "[email protected]",
"to" => ["[email protected]"],
"subject" => "Hello from Anypost",
"text" => "It worked.",
]);An official Ruby SDK is available for Ruby 3.2+ (source on GitHub):
gem install anypostrequire "anypost"
client = Anypost::Client.new("ap_your_api_key")
email = client.email.send(
from: "[email protected]",
to: ["[email protected]"],
subject: "Hello from Anypost",
text: "It worked."
)An official Rust SDK is available for Rust 1.75+, async with an optional blocking client (source on GitHub):
cargo add anypostuse anypost::{Client, SendEmail};
let client = Client::new("ap_your_api_key")?;
let email = client.email.send(
&SendEmail::new("[email protected]", ["[email protected]"])
.subject("Hello from Anypost")
.text("It worked."),
).await?;An official Go SDK is available for Go 1.23+, with zero dependencies (source on GitHub):
go get github.com/anypost/anypost-goimport "github.com/anypost/anypost-go"
client, _ := anypost.New("ap_your_api_key")
sent, _ := client.Email.Send(context.Background(), &anypost.SendEmailRequest{
From: "[email protected]",
To: []string{"[email protected]"},
Subject: "Hello from Anypost",
Text: "It worked.",
})An official Java SDK is available for Java 17+ (source on GitHub):
<dependency>
<groupId>com.anypost</groupId>
<artifactId>anypost-java</artifactId>
<version>0.1.0</version>
</dependency>import com.anypost.Anypost;
import com.anypost.model.SendEmailRequest;
Anypost client = Anypost.create("ap_your_api_key");
var sent = client.email.send(SendEmailRequest.builder()
.from("[email protected]")
.to("[email protected]")
.subject("Hello from Anypost")
.text("It worked.")
.build());An official .NET SDK is available for .NET 8+ (source on GitHub):
dotnet add package Anypostusing Anypost;
using Anypost.Models;
var client = AnypostClient.Create("ap_your_api_key");
var sent = await client.Email.SendAsync(new SendEmailRequest
{
From = "[email protected]",
To = ["[email protected]"],
Subject = "Hello from Anypost",
Text = "It worked.",
});Requests
Send request bodies as JSON with Content-Type: application/json, encoded
as UTF-8. Authenticate with a bearer token; see Authentication.
Request bodies are capped at 5 MB. A larger body is rejected with 413
before authentication or validation runs.
Resource IDs
Every resource has an ID prefixed with its type, in the form
{prefix}_{uuid}. The prefix tells you what an ID refers to at a glance, and
routes reject a mismatched prefix rather than silently missing.
| Prefix | Resource |
|---|---|
email_ | A sent message |
key_ | An API key |
domain_ | A sending domain |
template_ | A template |
wh_ | A webhook |
Responses and status codes
Successful responses return JSON, except 204, which has no body.
| Status | Meaning |
|---|---|
200 | The request succeeded. |
201 | A resource was created. |
202 | A message was accepted for delivery. |
204 | Success, with no response body. |
207 | A batch send completed with mixed per-entry outcomes; read the body. |
400, 422 | The request was invalid. |
401 | Authentication failed. |
403 | Authenticated, but not permitted to do this. |
404 | No such resource. |
409 | A request conflict — see Idempotency below. |
413 | The request body exceeded 5 MB. |
429 | A rate limit or send-volume limit was exceeded. See Sending limits. |
5xx | A server error. 502 and 503 are safe to retry. |
Errors
Every error response uses the same shape:
{
"error": {
"type": "validation_error",
"message": "The from field is required.",
"errors": {
"from": ["The from field is required."]
}
}
}typeis a stable, machine-readable string. Branch on this, not on the HTTP status or the human-readablemessage.messageis a single human-readable sentence.errorsappears only onvalidation_error. It maps each rejected field to a list of problems.
type | Status | Meaning |
|---|---|---|
validation_error | 400, 422 | The request body or query failed validation. |
authentication_error | 401 | The API key is missing or invalid. |
permission_error | 403 | The key may not perform this action. |
not_found | 404 | No such resource for this team. |
idempotency_concurrent | 409 | A request with the same Idempotency-Key is still in flight. |
idempotency_mismatch | 422 | An Idempotency-Key was reused with a different body. |
webhook_rotation_in_progress | 409 | A webhook signing-secret rotation is already in progress. |
rate_limit_exceeded | 429 | A rate limit was exceeded. |
provisioning_error | 503 | Anypost could not complete a provisioning step. Safe to retry. |
internal_error | 5xx | An unexpected server error. |
Two responses on the send endpoints fall outside this envelope: a 413
(oversized body, rejected at the transport layer) and a 429 send-volume
rejection, which returns a flat { "error": "quota_exceeded", "scope", ... }
body. See Sending limits for the 429 shape.
Pagination
List endpoints return results newest-first, in pages, using an opaque cursor.
| Parameter | What it does |
|---|---|
limit | Items per page, 1 to 100. Defaults to 20. |
after | A cursor from a previous response's next_cursor. |
Each page is shaped:
{
"data": [],
"has_more": true,
"next_cursor": "MjAyNi0wNC0zMFQx..."
}To fetch the next page, pass next_cursor as after. When has_more is
false, next_cursor is null and there are no more pages. Cursors are
opaque: do not parse or construct them.
The TypeScript and Python SDKs return a page you can read directly or iterate to walk every page:
const page = await anypost.domains.list({ limit: 50 });
page.data; // first page; page.next_cursor for the next
for await (const domain of await anypost.domains.list()) {
console.log(domain.name); // every domain, fetching pages as needed
}Idempotency
The send endpoints, POST /v1/email and POST /v1/email/batch, accept an
optional Idempotency-Key header so a retry cannot send twice. The key is 1
to 255 printable-ASCII characters.
- First use: the request runs normally and its response is stored for 24 hours.
- Reused with the same body: the stored response is returned verbatim. The message is not sent again.
- Reused with a different body: the request is rejected with
422idempotency_mismatch. Use a fresh key or send the original body. - Reused while the first request is still in flight: the second request
is rejected with
409idempotency_concurrent. Retry once it settles.
A request with no Idempotency-Key runs with no idempotency guarantee.
Server errors (5xx) are not stored, so retrying after one genuinely
retries the send.
Timestamps
All timestamps are ISO 8601 in UTC, with the Z marker and microsecond
precision:
2026-04-30T17:42:11.123456Z