Documentation
Base URL: https://api.sendoryn.com/v1. All requests and responses are JSON with camelCase keys.
Quickstart
- Create an account and verify your email address. New accounts are reviewed before sending is enabled.
- Add your sending domain under Domains and publish the DNS records shown.
- Click “Verify DNS”. Sending unlocks once ownership, SPF and DKIM verify.
- Create an API key with the
emails:sendscope. - Send your first message:
curl https://api.sendoryn.com/v1/emails \
-H "Authorization: Bearer gw_live_xxxxxxxxxxxx_your_secret" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: welcome-user-123" \
-d '{
"from": "Acme <[email protected]>",
"to": ["[email protected]"],
"subject": "Welcome",
"html": "<h1>Welcome!</h1>",
"text": "Welcome!",
"tags": ["welcome"],
"metadata": { "source": "website" }
}'A successful call returns 202 Accepted with { "id": "...", "status": "QUEUED" }. Queued means the message passed validation, authorization and quota checks — not that it was delivered.
Authentication
Send your API key as a bearer token: Authorization: Bearer gw_live_…. Keys are scoped; a request needing a scope the key lacks returns 403 forbidden. Keys can be restricted to IP ranges and given an expiry date. The full key is shown only once at creation — store it in a secret manager.
Domain verification
Each domain needs these DNS records. The dashboard shows the exact values for your domain with copy buttons.
| Record | Type | Host | Required |
|---|---|---|---|
Ownership | TXT | _gateway.yourdomain.com | Yes |
SPF | TXT | yourdomain.com — add include:<platform SPF domain> | Yes |
DKIM | TXT | <selector>._domainkey.yourdomain.com | Yes |
Return path | CNAME | gwb.yourdomain.com → platform bounce domain | Recommended |
DMARC | TXT | _dmarc.yourdomain.com | Recommended (required by Gmail/Yahoo for bulk senders) |
Publish only one SPF record per domain — if you already have one, add our include: to it. DNS changes can take minutes to hours to propagate; pending domains are re-checked automatically for 7 days, and active domains are re-validated daily. If records disappear, sending from that domain stops until they verify again.
Sending email
| Field | Type | Notes |
|---|---|---|
from | string | "Name <addr@domain>" or "addr@domain". Domain must be active in your account. |
to / cc / bcc | string | string[] | Up to 50 recipients in total. |
subject | string | Required unless a template supplies it. No line breaks. |
html / text | string | At least one, unless using a template. |
replyTo | string | Optional reply address. |
template | { id, variables } | Template ID or slug. All variables used by the template must be supplied. |
headers | object | Custom headers. Reserved headers (From, To, DKIM-Signature, …) are rejected. |
attachments | { filename, content, contentType }[] | Base64 content, up to 10 files / 7 MB. Executable types are blocked. |
tags | string[] | Up to 10 tags for filtering. |
metadata | object | Up to 20 string values, returned in events and webhooks. |
Pass an Idempotency-Key header to make retries safe: the same key returns the original result (200 with Idempotent-Replayed: true) instead of sending again.
SMTP
Create credentials under SMTP in the dashboard. Connect to mail.sendoryn.com on port 587 with STARTTLS and AUTH PLAIN or LOGIN. The From header must use a verified domain; credentials can be further restricted to specific domains. Each message may have up to 50 recipients.
SMTP_HOST=mail.sendoryn.com
SMTP_PORT=587
SMTP_SECURE=false # STARTTLS is negotiated on port 587
SMTP_USER=smtp_xxxxxxxxxxxxxxxx
SMTP_PASSWORD=your_smtp_passwordAPI keys
| Scope | Grants |
|---|---|
emails:send | Send and retry emails |
emails:read | List and read emails |
events:read | Read delivery events |
usage:read | Read usage and statistics |
domains:read / domains:write | Read or add and verify domains |
templates:read / templates:write | Read or manage templates |
webhooks:manage | Manage webhook endpoints |
api_keys:manage | Manage API keys (cannot grant scopes the key lacks) |
SDKs
Clients for TypeScript, Python and PHP ship in the source repository under packages/. They are not published to npm, PyPI or Packagist, so install them from a checkout. None has runtime dependencies beyond the language. The TypeScript package ships as source, so it needs a TypeScript-aware build such as a bundler or tsx.
# Copy packages/sdk into your project, then:
npm install ./vendor/gateway-sdkThe TypeScript package exports Gateway, the Python package gateway_email.Gateway, and the PHP package Gateway\\Email\\Gateway. Each takes your API key and your API base URL.
Templates
Templates use {{variable}} placeholders. Values are HTML-escaped in HTML bodies; there is no unescaped syntax. Editing a published template creates a new draft version, so live sends never change until you publish.
Marketing email
Marketing email is available once your account has been approved for it. Create a list at POST /v1/contact-lists, add people with a consent record at POST /v1/contact-lists/:id/contacts, then send with "marketing": {"listId": "..."} on POST /v1/emails or to a whole list with POST /v1/contact-lists/:id/broadcasts. Each message gets List-Unsubscribe one-click headers and a visible link; place {{unsubscribe_url}} to choose where it appears. An opt-out stops marketing only, so receipts and password resets still arrive. SMTP submission is for transactional mail.
Webhooks
Configure HTTPS endpoints and choose events. Each delivery is a POST with a JSON body:
{
"id": "msg_id:ACCEPTED",
"type": "email.accepted",
"created_at": "2026-11-09T10:15:02.000Z",
"data": {
"emailId": "lq2x9k3c8f1a2b3c4d5e6f",
"status": "ACCEPTED",
"from": "[email protected]",
"subject": "Welcome",
"recipient": "[email protected]",
"smtpCode": 250,
"response": "250 2.0.0 OK"
}
}| Event | When |
|---|---|
email.queued | Accepted by the API and queued |
email.processing | Handed to the sending pipeline |
email.accepted | Recipient server replied 250 |
email.deferred | Temporary failure; the MTA will retry |
email.failed | Permanent failure or retries exhausted |
email.bounced | Recipient rejected or bounced the message |
email.suppressed | Recipient is on a suppression list |
domain.verified / domain.verification_failed | DNS verification outcome |
quota.warning / quota.exhausted | 80% / 100% of monthly allowance |
Verify the Gateway-Signature header (t=…,v1=…) against the raw body, reject timestamps older than 5 minutes, and de-duplicate by Gateway-Event-Id. Non-2xx responses are retried with exponential backoff; an endpoint is disabled after 50 consecutive failed deliveries.
import crypto from "node:crypto";
// Use the raw request body (string), not a re-serialized object.
export function verify(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; // replay protection
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ""));
}
// verify(req.rawBody, req.headers["gateway-signature"], process.env.WEBHOOK_SECRET)Message statuses
| Status | Meaning |
|---|---|
QUEUED | Validated and waiting to be handed to the mail server. |
PROCESSING | Handed to our MTA; delivery to the recipient server is in progress. |
ACCEPTED | The recipient server accepted the message (SMTP 250). This is not proof of inbox placement. |
DEFERRED | Temporary failure (4xx). Our MTA retries on a schedule. |
BOUNCED | The recipient server permanently rejected the address or returned a bounce. |
FAILED | Permanent failure or retries exhausted. |
SUPPRESSED | Not sent because the recipient is on a suppression list. |
CANCELED | Canceled before hand-off. |
Error codes
| Code | HTTP | Meaning |
|---|---|---|
unauthorized | 401 | Missing, invalid, expired or revoked API key |
forbidden | 403 | Missing scope, role or IP not allowed |
sender_not_authorized | 403 | From domain is not an active domain of your account |
account_restricted | 403 | Account pending approval, restricted, suspended, or without an active plan |
plan_limit_reached | 403 | Plan limit for domains, keys, templates, etc. |
not_found | 404 | Resource does not exist in your organization |
conflict | 409 | State does not allow the operation |
payload_too_large | 413 | Message exceeds size limits |
validation_error | 422 | Invalid payload; see details |
rate_limited | 429 | Request or sending rate exceeded; see Retry-After |
quota_exhausted | 429 | Monthly allowance used and overage not enabled |
Rate limits
Two independent limits apply. Request rate: each API key may make 50 requests per second by default (configurable per key). Sending rate: your plan defines messages per second and per hour across all API keys and SMTP credentials. Exceeding either returns 429 with a Retry-After header. Monthly allowances are counted per recipient.
Troubleshooting
- sender_not_authorized — the From domain is not verified, is paused, or the SMTP credential is restricted to other domains.
- SPF “incorrect” — you probably have two SPF records. Merge them into one.
- Messages DEFERRED — the recipient server returned a temporary error (often rate-limiting or greylisting). Our MTA retries automatically.
- Accepted but not in the inbox — the recipient provider filtered it. Check DMARC alignment, content, list quality and complaint rates.
- SMTP 535 — wrong username or password, or the credential was revoked. 530 — you tried to send without authenticating.