Documentation

Base URL: https://api.sendoryn.com/v1. All requests and responses are JSON with camelCase keys.

Quickstart

  1. Create an account and verify your email address. New accounts are reviewed before sending is enabled.
  2. Add your sending domain under Domains and publish the DNS records shown.
  3. Click “Verify DNS”. Sending unlocks once ownership, SPF and DKIM verify.
  4. Create an API key with the emails:send scope.
  5. 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.

RecordTypeHostRequired
OwnershipTXT_gateway.yourdomain.comYes
SPFTXTyourdomain.com — add include:<platform SPF domain>Yes
DKIMTXT<selector>._domainkey.yourdomain.comYes
Return pathCNAMEgwb.yourdomain.com → platform bounce domainRecommended
DMARCTXT_dmarc.yourdomain.comRecommended (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

FieldTypeNotes
fromstring"Name <addr@domain>" or "addr@domain". Domain must be active in your account.
to / cc / bccstring | string[]Up to 50 recipients in total.
subjectstringRequired unless a template supplies it. No line breaks.
html / textstringAt least one, unless using a template.
replyTostringOptional reply address.
template{ id, variables }Template ID or slug. All variables used by the template must be supplied.
headersobjectCustom 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.
tagsstring[]Up to 10 tags for filtering.
metadataobjectUp 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_password

API keys

ScopeGrants
emails:sendSend and retry emails
emails:readList and read emails
events:readRead delivery events
usage:readRead usage and statistics
domains:read / domains:writeRead or add and verify domains
templates:read / templates:writeRead or manage templates
webhooks:manageManage webhook endpoints
api_keys:manageManage 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-sdk

The 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"
  }
}
EventWhen
email.queuedAccepted by the API and queued
email.processingHanded to the sending pipeline
email.acceptedRecipient server replied 250
email.deferredTemporary failure; the MTA will retry
email.failedPermanent failure or retries exhausted
email.bouncedRecipient rejected or bounced the message
email.suppressedRecipient is on a suppression list
domain.verified / domain.verification_failedDNS verification outcome
quota.warning / quota.exhausted80% / 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

StatusMeaning
QUEUEDValidated and waiting to be handed to the mail server.
PROCESSINGHanded to our MTA; delivery to the recipient server is in progress.
ACCEPTEDThe recipient server accepted the message (SMTP 250). This is not proof of inbox placement.
DEFERREDTemporary failure (4xx). Our MTA retries on a schedule.
BOUNCEDThe recipient server permanently rejected the address or returned a bounce.
FAILEDPermanent failure or retries exhausted.
SUPPRESSEDNot sent because the recipient is on a suppression list.
CANCELEDCanceled before hand-off.

Error codes

CodeHTTPMeaning
unauthorized401Missing, invalid, expired or revoked API key
forbidden403Missing scope, role or IP not allowed
sender_not_authorized403From domain is not an active domain of your account
account_restricted403Account pending approval, restricted, suspended, or without an active plan
plan_limit_reached403Plan limit for domains, keys, templates, etc.
not_found404Resource does not exist in your organization
conflict409State does not allow the operation
payload_too_large413Message exceeds size limits
validation_error422Invalid payload; see details
rate_limited429Request or sending rate exceeded; see Retry-After
quota_exhausted429Monthly 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.