Skip to content

REST API Reference

https://api.relaypost.dev/v1

All requests require an API key in the Authorization header:

Authorization: Bearer YOUR_API_KEY

API keys are created in the RelayPost dashboard under Settings → API Keys. See API Keys for details.

All successful responses are wrapped in a data envelope:

{
"data": { ... }
}

Paginated responses include a pagination object:

{
"data": [...],
"pagination": {
"page": 1,
"limit": 20,
"total_count": 150,
"total_pages": 8
}
}

Error responses use a consistent error envelope:

{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{ "field": "to", "message": "At least one recipient is required" }
]
}
}

See Error Codes for the full list.

Every response includes rate limit headers. See Rate Limits for details.

HeaderDescription
X-RateLimit-LimitMaximum requests allowed in the current window
X-RateLimit-RemainingRequests remaining in the current window
Retry-AfterSeconds to wait before retrying (only on 429 responses)

POST /api/v1/emails/send

Terminal window
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>"
}'
FieldTypeRequiredDescription
fromobjectYesSender — { "email": "...", "name": "..." }
toarrayYesRecipients — [{ "email": "...", "name": "..." }]
ccarrayNoCC recipients
bccarrayNoBCC recipients
subjectstringYesEmail subject line
htmlstringNoHTML body
textstringNoPlain text body
template_idstringNoUse a saved template instead of inline content
template_dataobjectNoKey-value pairs for template variables
headersobjectNoCustom email headers
scheduled_atstringNoISO 8601 datetime for scheduled delivery

You must provide either html, text, or template_id.

{
"data": {
"message_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "queued",
"queued_at": "2025-01-15T10:30:00.000Z"
}
}
StatusCodeDescription
400VALIDATION_ERRORMissing or invalid fields
400DOMAIN_NOT_VERIFIEDFrom address uses an unverified domain
400RECIPIENTS_SUPPRESSEDOne or more recipients are on the suppression list
400TEMPLATE_INACTIVETemplate is not active
400TEMPLATE_MISSING_VARIABLESRequired template variables missing
400TEMPLATE_DATA_INVALIDTemplate data type mismatch
404TEMPLATE_NOT_FOUNDTemplate ID not found or belongs to another org
409TEMPLATE_NAME_CONFLICTTemplate name already exists (on create)
429LIMIT_EXCEEDEDOrganization email sending limit exceeded

GET /api/v1/emails

ParameterTypeDefaultDescription
statusstringFilter by status: queued, delivered, bounced, failed, opened
from_datestringISO 8601 start date
to_datestringISO 8601 end date
pageinteger1Page number
limitinteger20Results per page (max 100)
{
"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": 20,
"total_count": 42,
"total_pages": 3
}
}

GET /api/v1/emails/:id

Retrieve a specific email by its ID or message ID, including delivery events.

{
"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"
}
]
}
}
StatusCodeDescription
404NOT_FOUNDEmail not found or belongs to another organization

POST /api/v1/emails/validate

Run the full validation pipeline on a single email address. Returns a risk score, verdict, and detailed check results.

Terminal window
curl -X POST https://api.relaypost.dev/v1/emails/validate \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{ "email": "user@example.com" }'
FieldTypeRequiredDescription
emailstringYesThe email address to validate
{
"data": {
"email": "user@example.com",
"verdict": "deliverable",
"risk_score": 0.0,
"checks": {
"syntax": { "status": "pass" },
"mx": {
"status": "pass",
"records": [{ "priority": 10, "exchange": "mx.example.com" }]
},
"dns": { "status": "pass", "addresses": ["93.184.216.34"] },
"disposable": { "status": "pass" },
"role": { "status": "pass" },
"typo": { "status": "pass", "suggestions": [] },
"free_provider": { "status": "pass", "provider_type": "unknown" },
"random": { "status": "pass" }
},
"suggestions": [],
"timestamp": "2025-01-15T10:30:00.000Z"
}
}
VerdictDescription
deliverableEmail is likely valid and safe to send to
riskyEmail may have issues — disposable, role-based, or typo
undeliverableEmail is invalid — bad syntax, no MX records, or nonexistent
StatusDescription
passCheck passed — no issues found
failCheck failed — issue detected
warnAdvisory warning — not necessarily a problem
unknownCheck could not be completed (e.g. DNS timeout)
StatusCodeDescription
400VALIDATION_ERRORMissing or invalid email field

POST /api/v1/emails/validate/batch

Validate up to 100 email addresses in a single request.

Terminal window
curl -X POST https://api.relaypost.dev/v1/emails/validate/batch \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"emails": [
"user@example.com",
"invalid@",
"test@tempmail.com"
]
}'
FieldTypeRequiredDescription
emailsstring[]YesArray of email addresses to validate (max 100)
{
"data": [
{
"email": "user@example.com",
"verdict": "deliverable",
"risk_score": 0.0,
"checks": { ... },
"suggestions": [],
"timestamp": "2025-01-15T10:30:00.000Z"
},
{
"email": "invalid@",
"verdict": "undeliverable",
"risk_score": 1.0,
"checks": { ... },
"suggestions": [],
"timestamp": "2025-01-15T10:30:00.000Z"
}
]
}

Each item in the response array has the same shape as the single validation response.

StatusCodeDescription
400VALIDATION_ERRORMissing emails field, not an array, or exceeds 100

GET /api/v1/settings/auto-validation

Returns the current auto-validation threshold for your organization. When enabled, emails are automatically validated before sending and rejected if they exceed the risk threshold.

Terminal window
curl "https://api.relaypost.dev/v1/settings/auto-validation" \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": {
"auto_validation_threshold": "off"
}
}

PUT /api/v1/settings/auto-validation

Terminal window
curl -X PUT https://api.relaypost.dev/v1/settings/auto-validation \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{ "threshold": "high" }'
FieldTypeRequiredDescription
thresholdstringYesOne of: high, medium, off (default)
ValueBehavior
highReject only undeliverable emails (risk score > 0.7)
mediumReject undeliverable and risky emails (risk score > 0.3)
offNo automatic validation — all emails are sent regardless of risk score
{
"data": {
"auto_validation_threshold": "high"
}
}
StatusCodeDescription
400VALIDATION_ERRORInvalid threshold value

POST /api/v1/domains

FieldTypeRequiredDescription
domainstringYesThe domain name (e.g. example.com)
{
"data": {
"id": "dom_abc123",
"domain": "example.com",
"is_verified": false,
"spf_verified": false,
"dkim_verified": false,
"dkim_selector": "relaypost",
"dkim_public_key": "MIGfMA0GCSqGSIb3DQEBAQUAA4...",
"dns_records": [
{
"type": "TXT",
"name": "relaypost._domainkey.example.com",
"value": "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4..."
},
{
"type": "TXT",
"name": "example.com",
"value": "v=spf1 include:relaypost.dev ~all"
}
],
"created_at": "2025-01-15T10:30:00.000Z"
}
}

GET /api/v1/domains

{
"data": [
{
"id": "dom_abc123",
"domain": "example.com",
"is_verified": true,
"spf_verified": true,
"dkim_verified": true,
"dkim_selector": "relaypost",
"dkim_public_key": "MIGfMA0GCSqGSIb3DQEBAQUAA4...",
"verified_at": "2025-01-15T11:00:00.000Z",
"created_at": "2025-01-15T10:30:00.000Z"
}
]
}

GET /api/v1/domains/:id/verify

Performs DNS verification and returns the updated domain status.

{
"data": {
"id": "dom_abc123",
"domain": "example.com",
"is_verified": true,
"spf_verified": true,
"dkim_verified": true,
"dkim_selector": "relaypost",
"dkim_public_key": "MIGfMA0GCSqGSIb3DQEBAQUAA4...",
"verified_at": "2025-01-15T11:00:00.000Z",
"created_at": "2025-01-15T10:30:00.000Z"
}
}
StatusCodeDescription
404NOT_FOUNDDomain not found or belongs to another organization

GET /api/v1/suppressions

ParameterTypeDefaultDescription
pageinteger1Page number
limitinteger20Results per page (max 100)
{
"data": [
{
"id": "sup_abc123",
"email": "bounced@example.com",
"reason": "hard_bounce",
"source": "automatic",
"created_at": "2025-01-15T10:30:00.000Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total_count": 15,
"total_pages": 1
}
}

POST /api/v1/suppressions

FieldTypeRequiredDescription
emailstringYesEmail address to suppress
reasonstringYesOne of: hard_bounce, soft_bounce, complaint, unsubscribe, manual
{
"data": {
"id": "sup_def456",
"email": "bounced@example.com",
"reason": "hard_bounce",
"source": "manual",
"created_at": "2025-01-15T10:30:00.000Z"
}
}
StatusCodeDescription
400VALIDATION_ERRORInvalid email or reason

DELETE /api/v1/suppressions/:id

{
"data": {
"id": "sup_abc123",
"deleted": true
}
}
StatusCodeDescription
404NOT_FOUNDSuppression not found or belongs to another organization

GET /api/v1/templates

ParameterTypeDefaultDescription
pageinteger1Page number
limitinteger20Results per page (max 100)
{
"data": [
{
"id": "tmpl_abc123",
"name": "Order Confirmation",
"subject": "Your order #{{orderNumber}}",
"html_body": "...",
"text_body": "...",
"variables": ["orderNumber", "customerName", "total"],
"variable_schema": null,
"is_active": true,
"created_at": "2025-01-15T10:30:00.000Z",
"updated_at": "2025-01-15T10:30:00.000Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total_count": 5,
"total_pages": 1
}
}

POST /api/v1/templates

FieldTypeRequiredDescription
namestringYesTemplate name
subjectstringYesSubject line (supports {{variables}})
htmlstringNoHTML body (supports {{variables}})
textstringNoPlain text body (supports {{variables}})
variable_schemaarrayNoTyped variable definitions — see Variable Schema below
{
"data": {
"id": "tmpl_abc123",
"name": "Order Confirmation",
"subject": "Your order #{{orderNumber}}",
"html_body": "<h1>Thanks, {{customerName}}!</h1>",
"text_body": "Thanks, {{customerName}}!",
"variables": ["orderNumber", "customerName"],
"variable_schema": null,
"is_active": true,
"created_at": "2025-01-15T10:30:00.000Z",
"updated_at": "2025-01-15T10:30:00.000Z"
}
}

PUT /api/v1/templates/:id

Only include the fields you want to change.

FieldTypeRequiredDescription
namestringNoUpdated template name
subjectstringNoUpdated subject line
htmlstringNoUpdated HTML body
textstringNoUpdated plain text body
variable_schemaarrayNoUpdated variable definitions
is_activebooleanNoActivate or deactivate

At least one field must be provided.

{
"data": {
"id": "tmpl_abc123",
"name": "Order Confirmation",
"subject": "Your order #{{orderNumber}}",
"html_body": "<h1>Updated layout</h1>",
"text_body": "Thanks, {{customerName}}!",
"variables": ["orderNumber", "customerName"],
"variable_schema": null,
"is_active": true,
"created_at": "2025-01-15T10:30:00.000Z",
"updated_at": "2025-01-15T11:00:00.000Z"
}
}
StatusCodeDescription
400VALIDATION_ERRORNo update fields provided or invalid values
404NOT_FOUNDTemplate not found or belongs to another organization
409TEMPLATE_NAME_CONFLICTTemplate name already exists

DELETE /api/v1/templates/:id

Soft-deletes the template by setting is_active to false. The template remains in the database so historical email records referencing it stay valid. Use PUT /api/v1/templates/:id with is_active: true to reactivate.

{
"data": {
"id": "tmpl_abc123",
"name": "Order Confirmation",
"subject": "Your order #{{orderNumber}}",
"html_body": "<h1>Thanks, {{customerName}}!</h1>",
"text_body": "Thanks, {{customerName}}!",
"variables": ["orderNumber", "customerName"],
"variable_schema": null,
"is_active": false,
"created_at": "2025-01-15T10:30:00.000Z",
"updated_at": "2025-01-15T12:00:00.000Z"
}
}
StatusCodeDescription
404NOT_FOUNDTemplate not found or belongs to another organization

Templates support an optional variable_schema field that defines typed variable declarations. When provided, template_data is validated against the schema at send time.

Each entry in the array has the following shape:

FieldTypeRequiredDescription
namestringYesVariable name as referenced in template content
typestringYesstring, number, boolean, array, or object
requiredbooleanYesWhether the variable must be provided at send time
default_valueanyNoDefault value when the variable is not provided (must match type)

Example schema:

{
"variable_schema": [
{ "name": "customerName", "type": "string", "required": true },
{ "name": "orderNumber", "type": "string", "required": true },
{ "name": "total", "type": "string", "required": true },
{ "name": "items", "type": "array", "required": false, "default_value": [] }
]
}

Variables are also auto-detected from {{variable}} placeholders in the subject, HTML, and text bodies. The variables field in the response lists all detected variables. If a variable appears in the content but is not declared in variable_schema, a warning is returned in the response.


POST /api/v1/templates/:id/preview

Preview a rendered template with sample data. Missing variables render as empty strings with warnings. Inactive templates can be previewed.

Terminal window
curl -X POST https://api.relaypost.dev/v1/templates/tmpl_abc123/preview \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"data": {
"customerName": "Jane",
"orderNumber": "1234",
"total": "$49.99"
}
}'
FieldTypeRequiredDescription
dataobjectYesSample data for template variable substitution
{
"data": {
"subject": "Your order #1234",
"html": "<h1>Thanks, Jane!</h1><p>Your order #1234 totaling $49.99 has been confirmed.</p>",
"text": "Thanks, Jane! Your order #1234 totaling $49.99 has been confirmed.",
"variables": ["orderNumber", "customerName", "total"],
"warnings": []
}
}

When variables are missing, they render as empty strings and appear in warnings:

{
"data": {
"subject": "Your order #",
"html": "<h1>Thanks, Jane!</h1><p>Your order # totaling has been confirmed.</p>",
"text": "Thanks, Jane! Your order # totaling has been confirmed.",
"variables": ["orderNumber", "customerName", "total"],
"warnings": ["Missing variable: orderNumber", "Missing variable: total"]
}
}
StatusCodeDescription
400VALIDATION_ERRORMissing or invalid data field
404TEMPLATE_NOT_FOUNDTemplate not found or belongs to another organization

POST /api/v1/emails/batch

Send the same template to multiple recipients with per-recipient variable data. Maximum 1000 recipients per batch.

Terminal window
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" },
"subject": "Your order is confirmed",
"recipients": [
{
"to": { "email": "alice@example.com", "name": "Alice" },
"data": { "customerName": "Alice", "orderNumber": "ORD-001", "total": "$29.99" }
},
{
"to": { "email": "bob@example.com", "name": "Bob" },
"data": { "customerName": "Bob", "orderNumber": "ORD-002", "total": "$59.99" }
}
]
}'
FieldTypeRequiredDescription
template_idstringYesTemplate ID to use for all recipients
fromobjectYesSender — { "email": "...", "name": "..." }
subjectstringNoOptional subject override (replaces template subject)
recipientsarrayYesArray of recipients (max 1000), each with to and data

Each item in recipients:

FieldTypeRequiredDescription
toobjectYesRecipient — { "email": "...", "name": "..." }
dataobjectYesPer-recipient template variable data
{
"data": {
"batch_id": "550e8400-e29b-41d4-a716-446655440000",
"total": 2,
"accepted": 2,
"rejected": []
}
}

Partial rejection example:

{
"data": {
"batch_id": "550e8400-e29b-41d4-a716-446655440000",
"total": 3,
"accepted": 2,
"rejected": [
{
"index": 1,
"to": "bob@example.com",
"error": "Missing required variable: orderNumber"
}
]
}
}
StatusCodeDescription
400BATCH_TOO_LARGEExceeds 1000 recipient limit
400VALIDATION_ERRORMissing required fields or invalid request body
400TEMPLATE_INACTIVETemplate is not active
402BILLING_LIMIT_EXCEEDEDBatch would exceed your plan’s email limit
404TEMPLATE_NOT_FOUNDTemplate not found or belongs to another organization