Docs/API reference

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 anypost
import { 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 anypost
from 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-php
use 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 anypost
require "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 anypost
use 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-go
import "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 Anypost
using 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.

PrefixResource
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.

StatusMeaning
200The request succeeded.
201A resource was created.
202A message was accepted for delivery.
204Success, with no response body.
207A batch send completed with mixed per-entry outcomes; read the body.
400, 422The request was invalid.
401Authentication failed.
403Authenticated, but not permitted to do this.
404No such resource.
409A request conflict — see Idempotency below.
413The request body exceeded 5 MB.
429A rate limit or send-volume limit was exceeded. See Sending limits.
5xxA 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."]
        }
    }
}
  • type is a stable, machine-readable string. Branch on this, not on the HTTP status or the human-readable message.
  • message is a single human-readable sentence.
  • errors appears only on validation_error. It maps each rejected field to a list of problems.
typeStatusMeaning
validation_error400, 422The request body or query failed validation.
authentication_error401The API key is missing or invalid.
permission_error403The key may not perform this action.
not_found404No such resource for this team.
idempotency_concurrent409A request with the same Idempotency-Key is still in flight.
idempotency_mismatch422An Idempotency-Key was reused with a different body.
webhook_rotation_in_progress409A webhook signing-secret rotation is already in progress.
rate_limit_exceeded429A rate limit was exceeded.
provisioning_error503Anypost could not complete a provisioning step. Safe to retry.
internal_error5xxAn 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.

ParameterWhat it does
limitItems per page, 1 to 100. Defaults to 20.
afterA 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 422 idempotency_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 409 idempotency_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