Send via API
Send an email
Section titled “Send an email”POST /api/v1/emails/send
curl -X POST https://api.relaypost.dev/v1/emails/send \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "from": { "email": "hello@yourdomain.com", "name": "Your App" }, "to": [{ "email": "user@example.com" }], "subject": "Your receipt", "html": "<h1>Thanks for your purchase!</h1><p>Order #1234</p>" }'const response = await fetch("https://api.relaypost.dev/v1/emails/send", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": "Bearer YOUR_API_KEY", }, body: JSON.stringify({ from: { email: "hello@yourdomain.com", name: "Your App" }, to: [{ email: "user@example.com" }], subject: "Your receipt", html: "<h1>Thanks for your purchase!</h1><p>Order #1234</p>", }),});
const result = await response.json();console.log(result.data.message_id);import requests
response = requests.post( "https://api.relaypost.dev/v1/emails/send", headers={ "Content-Type": "application/json", "Authorization": "Bearer YOUR_API_KEY", }, json={ "from": {"email": "hello@yourdomain.com", "name": "Your App"}, "to": [{"email": "user@example.com"}], "subject": "Your receipt", "html": "<h1>Thanks for your purchase!</h1><p>Order #1234</p>", },)
result = response.json()print(result["data"]["message_id"])require "net/http"require "json"require "uri"
uri = URI("https://api.relaypost.dev/v1/emails/send")http = Net::HTTP.new(uri.host, uri.port)http.use_ssl = true
request = Net::HTTP::Post.new(uri)request["Content-Type"] = "application/json"request["Authorization"] = "Bearer YOUR_API_KEY"request.body = {from: { email: "hello@yourdomain.com", name: "Your App" },to: [{ email: "user@example.com" }],subject: "Your receipt",html: "<h1>Thanks for your purchase!</h1><p>Order #1234</p>"}.to_json
response = http.request(request)data = JSON.parse(response.body)["data"]puts data["message_id"]use GuzzleHttp\Client;
$client = new Client(["base_uri" => "https://api.relaypost.dev"]);
$response = $client->post("/v1/emails/send", [ "headers" => ["Authorization" => "Bearer YOUR_API_KEY"], "json" => [ "from" => ["email" => "hello@yourdomain.com", "name" => "Your App"], "to" => [["email" => "user@example.com"]], "subject" => "Your receipt", "html" => "<h1>Thanks for your purchase!</h1><p>Order #1234</p>", ],]);
$data = json_decode($response->getBody(), true)["data"];echo $data["message_id"];Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
from | object | Yes | Sender — { "email": "...", "name": "..." } |
to | array | Yes | Recipients — [{ "email": "...", "name": "..." }] |
cc | array | No | CC recipients |
bcc | array | No | BCC recipients |
subject | string | Yes | Email subject line |
html | string | No | HTML body |
text | string | No | Plain text body |
template_id | string | No | Use a saved template instead of inline content |
template_data | object | No | Key-value pairs for template variables |
headers | object | No | Custom email headers |
scheduled_at | string | No | ISO 8601 datetime for scheduled delivery |
You must provide either html, text, or template_id.
Response (201)
Section titled “Response (201)”{ "data": { "message_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "queued", "queued_at": "2025-01-15T10:30:00.000Z" }}Errors
Section titled “Errors”| Status | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | Missing or invalid fields |
| 400 | DOMAIN_NOT_VERIFIED | From address uses an unverified domain |
| 400 | RECIPIENTS_SUPPRESSED | One or more recipients are on the suppression list |
| 429 | LIMIT_EXCEEDED | Organization email sending limit exceeded |
Examples
Section titled “Examples”{ "from": { "email": "orders@yourapp.com" }, "to": [{ "email": "customer@example.com" }], "subject": "Your receipt", "template_id": "tmpl_abc123", "template_data": { "order_number": "1234", "total": "$49.99" }}{ "from": { "email": "noreply@yourapp.com" }, "to": [{ "email": "user@example.com" }], "subject": "Weekly digest", "html": "<p>Here's what happened this week...</p>", "scheduled_at": "2026-02-14T09:00:00Z"}{ "from": { "email": "team@yourapp.com", "name": "Team" }, "to": [ { "email": "alice@example.com", "name": "Alice" }, { "email": "bob@example.com", "name": "Bob" } ], "cc": [{ "email": "manager@example.com" }], "subject": "Meeting notes", "text": "Here are the notes from today's meeting."}Check email status
Section titled “Check email status”GET /api/v1/emails/:id
Retrieve a specific email by its ID or message ID, including delivery events.
curl https://api.relaypost.dev/v1/emails/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ -H "Authorization: Bearer YOUR_API_KEY"const response = await fetch( "https://api.relaypost.dev/v1/emails/a1b2c3d4-e5f6-7890-abcd-ef1234567890", { headers: { "Authorization": "Bearer YOUR_API_KEY" }, });
const result = await response.json();console.log(result.data.status);import requests
response = requests.get( "https://api.relaypost.dev/v1/emails/a1b2c3d4-e5f6-7890-abcd-ef1234567890", headers={"Authorization": "Bearer YOUR_API_KEY"},)
result = response.json()print(result["data"]["status"])require "net/http"require "json"require "uri"
email_id = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"uri = URI("https://api.relaypost.dev/v1/emails/#{email_id}")http = Net::HTTP.new(uri.host, uri.port)http.use_ssl = true
request = Net::HTTP::Get.new(uri)request["Authorization"] = "Bearer YOUR_API_KEY"
response = http.request(request)data = JSON.parse(response.body)["data"]puts data["status"]use GuzzleHttp\Client;
$client = new Client(["base_uri" => "https://api.relaypost.dev"]);$emailId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
$response = $client->get("/v1/emails/{$emailId}", [ "headers" => ["Authorization" => "Bearer YOUR_API_KEY"],]);
$data = json_decode($response->getBody(), true)["data"];echo $data["status"];Response (200)
Section titled “Response (200)”{ "data": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "from_address": "hello@yourdomain.com", "to_addresses": ["user@example.com"], "subject": "Your receipt", "status": "delivered", "priority": "normal", "created_at": "2025-01-15T10:30:00.000Z", "updated_at": "2025-01-15T10:30:05.000Z", "scheduled_at": null, "events": [ { "id": "evt_001", "type": "delivered", "recipient": "user@example.com", "smtp_code": 250, "smtp_message": "OK", "created_at": "2025-01-15T10:30:05.000Z" } ] }}Errors
Section titled “Errors”| Status | Code | Description |
|---|---|---|
| 404 | NOT_FOUND | Email not found or belongs to another organization |
List emails
Section titled “List emails”GET /api/v1/emails
Retrieve a paginated list of emails for your organization.
curl "https://api.relaypost.dev/v1/emails?status=delivered&page=1&limit=10" \ -H "Authorization: Bearer YOUR_API_KEY"const params = new URLSearchParams({ status: "delivered", page: "1", limit: "10",});
const response = await fetch(`https://api.relaypost.dev/v1/emails?${params}`,{headers: { "Authorization": "Bearer YOUR_API_KEY" },});
const result = await response.json();console.log(result.data);console.log(result.pagination);import requests
response = requests.get( "https://api.relaypost.dev/v1/emails", headers={"Authorization": "Bearer YOUR_API_KEY"}, params={"status": "delivered", "page": 1, "limit": 10},)
result = response.json()print(result["data"])print(result["pagination"])require "net/http"require "json"require "uri"
uri = URI("https://api.relaypost.dev/v1/emails")uri.query = URI.encode_www_form(status: "delivered", page: 1, limit: 10)http = Net::HTTP.new(uri.host, uri.port)http.use_ssl = true
request = Net::HTTP::Get.new(uri)request["Authorization"] = "Bearer YOUR_API_KEY"
response = http.request(request)result = JSON.parse(response.body)puts result["data"]puts result["pagination"]use GuzzleHttp\Client;
$client = new Client(["base_uri" => "https://api.relaypost.dev"]);
$response = $client->get("/v1/emails", [ "headers" => ["Authorization" => "Bearer YOUR_API_KEY"], "query" => ["status" => "delivered", "page" => 1, "limit" => 10],]);
$result = json_decode($response->getBody(), true);print_r($result["data"]);print_r($result["pagination"]);Query parameters
Section titled “Query parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
status | string | — | Filter by status: queued, delivered, bounced, failed, opened |
from_date | string | — | ISO 8601 start date |
to_date | string | — | ISO 8601 end date |
page | integer | 1 | Page number |
limit | integer | 20 | Results per page (max 100) |
Response (200)
Section titled “Response (200)”{ "data": [ { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "from_address": "hello@yourdomain.com", "to_addresses": ["user@example.com"], "subject": "Your receipt", "status": "delivered", "priority": "normal", "created_at": "2025-01-15T10:30:00.000Z", "updated_at": "2025-01-15T10:30:05.000Z" } ], "pagination": { "page": 1, "limit": 10, "total_count": 42, "total_pages": 5 }}Batch send with templates
Section titled “Batch send with templates”POST /api/v1/emails/batch
Send the same template to multiple recipients with per-recipient variable data. Maximum 1000 recipients per batch.
curl -X POST https://api.relaypost.dev/v1/emails/batch \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "template_id": "tmpl_abc123", "from": { "email": "orders@yourapp.com", "name": "Your Store" }, "recipients": [ { "to": { "email": "alice@example.com" }, "data": { "customerName": "Alice", "orderNumber": "ORD-001" } }, { "to": { "email": "bob@example.com" }, "data": { "customerName": "Bob", "orderNumber": "ORD-002" } } ] }'const response = await fetch("https://api.relaypost.dev/v1/emails/batch", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": "Bearer YOUR_API_KEY", }, body: JSON.stringify({ template_id: "tmpl_abc123", from: { email: "orders@yourapp.com", name: "Your Store" }, recipients: [ { to: { email: "alice@example.com" }, data: { customerName: "Alice", orderNumber: "ORD-001" } }, { to: { email: "bob@example.com" }, data: { customerName: "Bob", orderNumber: "ORD-002" } }, ], }),});
const result = await response.json();console.log(`Batch ${result.data.batch_id}: ${result.data.accepted}/${result.data.total} accepted`);import requests
response = requests.post( "https://api.relaypost.dev/v1/emails/batch", headers={"Authorization": "Bearer YOUR_API_KEY"}, json={ "template_id": "tmpl_abc123", "from": {"email": "orders@yourapp.com", "name": "Your Store"}, "recipients": [ {"to": {"email": "alice@example.com"}, "data": {"customerName": "Alice", "orderNumber": "ORD-001"}}, {"to": {"email": "bob@example.com"}, "data": {"customerName": "Bob", "orderNumber": "ORD-002"}}, ], },)
data = response.json()["data"]print(f"Batch {data['batch_id']}: {data['accepted']}/{data['total']} accepted")require "net/http"require "json"require "uri"
uri = URI("https://api.relaypost.dev/v1/emails/batch")http = Net::HTTP.new(uri.host, uri.port)http.use_ssl = true
request = Net::HTTP::Post.new(uri)request["Content-Type"] = "application/json"request["Authorization"] = "Bearer YOUR_API_KEY"request.body = {template_id: "tmpl_abc123",from: { email: "orders@yourapp.com", name: "Your Store" },recipients: [{ to: { email: "alice@example.com" }, data: { customerName: "Alice", orderNumber: "ORD-001" } },{ to: { email: "bob@example.com" }, data: { customerName: "Bob", orderNumber: "ORD-002" } }]}.to_json
response = http.request(request)data = JSON.parse(response.body)["data"]puts "Batch #{data["batch_id"]}: #{data["accepted"]}/#{data["total"]} accepted"use GuzzleHttp\Client;
$client = new Client(["base_uri" => "https://api.relaypost.dev"]);
$response = $client->post("/v1/emails/batch", [ "headers" => ["Authorization" => "Bearer YOUR_API_KEY"], "json" => [ "template_id" => "tmpl_abc123", "from" => ["email" => "orders@yourapp.com", "name" => "Your Store"], "recipients" => [ ["to" => ["email" => "alice@example.com"], "data" => ["customerName" => "Alice", "orderNumber" => "ORD-001"]], ["to" => ["email" => "bob@example.com"], "data" => ["customerName" => "Bob", "orderNumber" => "ORD-002"]], ], ],]);
$data = json_decode($response->getBody(), true)["data"];echo "Batch {$data['batch_id']}: {$data['accepted']}/{$data['total']} accepted\n";Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
template_id | string | Yes | Template ID to use for all recipients |
from | object | Yes | Sender — { "email": "...", "name": "..." } |
subject | string | No | Optional subject override (replaces template subject) |
recipients | array | Yes | Array of recipients (max 1000), each with to and data |
Response (200)
Section titled “Response (200)”{ "data": { "batch_id": "550e8400-e29b-41d4-a716-446655440000", "total": 2, "accepted": 2, "rejected": [] }}Errors
Section titled “Errors”| Status | Code | Description |
|---|---|---|
| 400 | BATCH_TOO_LARGE | Exceeds 1000 recipient limit |
| 400 | TEMPLATE_INACTIVE | Template is not active |
| 402 | BILLING_LIMIT_EXCEEDED | Batch would exceed your plan’s email limit |
| 404 | TEMPLATE_NOT_FOUND | Template not found |
Email lifecycle
Section titled “Email lifecycle”Every email goes through these statuses:
queued → processing → delivered → opened ↘ bounced ↘ failed ↘ rejected| Status | Meaning |
|---|---|
queued | Accepted and waiting in the delivery queue |
scheduled | Queued for future delivery at scheduled_at |
processing | Currently being sent |
delivered | Successfully delivered to the recipient’s mail server |
opened | Recipient opened the email (if tracking is enabled) |
bounced | Recipient’s mail server rejected the email |
rejected | RelayPost rejected the email (suppression list, validation failure) |
failed | Delivery failed after all retry attempts |