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 email
GET/v1/emailsList emails with filters
GET/v1/emails/:idEmail with timeline and attempts
POST/v1/emails/:id/retryRetry a failed email
GET/v1/eventsDelivery events
GET/v1/usageUsage and plan limits
GET/v1/domainsList domains
POST/v1/domainsAdd a domain
POST/v1/domains/:id/verifyCheck DNS records
GET/v1/api-keysList API keys
POST/v1/api-keysCreate an API key
DELETE/v1/api-keys/:idRevoke an API key
GET/v1/templatesList templates
POST/v1/templatesCreate a template
GET/v1/webhooksList webhook endpoints
POST/v1/webhooksCreate a webhook endpoint

Verify 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)