Developers
An API that tells you the truth
Predictable resources, idempotent writes, explicit error codes and an event stream that reflects what mail servers actually said.
Send your first email
Create an API key with the emails:send scope, verify a domain, and post to /v1/emails. The response returns as soon as the message is queued.
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" }
}'Authentication
Bearer API keys with scopes, expiry and optional IP allowlists. Keys are shown once and stored as hashes.
Errors
Every error returns { error: { code, message, details } } with a stable code such as sender_not_authorized or quota_exhausted.
Webhooks
Events are signed with HMAC-SHA256 over the timestamp and raw body. Reject stale timestamps and de-duplicate by event ID.
OpenAPI
The full specification is published as OpenAPI 3.1 for client generation and API tooling.
Endpoints
POST
/v1/emailsSend an emailGET
/v1/emailsList emails with filtersGET
/v1/emails/:idEmail with timeline and attemptsPOST
/v1/emails/:id/retryRetry a failed emailGET
/v1/eventsDelivery eventsGET
/v1/usageUsage and plan limitsGET
/v1/domainsList domainsPOST
/v1/domainsAdd a domainPOST
/v1/domains/:id/verifyCheck DNS recordsGET
/v1/api-keysList API keysPOST
/v1/api-keysCreate an API keyDELETE
/v1/api-keys/:idRevoke an API keyGET
/v1/templatesList templatesPOST
/v1/templatesCreate a templateGET
/v1/webhooksList webhook endpointsPOST
/v1/webhooksCreate a webhook endpointVerify webhook signatures
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)