API reference

Manage domains, addresses and SMTP passwords for Sendas. Create an API key in the dashboard and send it as `Authorization: Bearer sk_...`.

Authentication

Create a key under API keys and send it with every request. Keys can do everything the dashboard can, except manage other keys.

curl https://sendas.space/api/v1/me \
  -H "Authorization: Bearer sk_..."

Responses are JSON: { "data": … } on success and { "error": { "code", "message" } } on failure.

Webhooks

Add an HTTPS endpoint under Webhooks or with POST /api/v1/webhooks, and Sendas posts an event within seconds of each message: message.received, message.sent, message.bounced and message.rejected. Events carry the same metadata as the activity log, never the message body.

{
  "id": "7d0c5c0e-5a1b-4f7e-9a51-0b6f1f3c2d11",
  "type": "message.received",
  "createdAt": "2026-11-03T08:15:02.114Z",
  "data": {
    "id": "18342",
    "domain": "yourcompany.com",
    "address": "hr@yourcompany.com",
    "direction": "inbound",
    "status": "forwarded",
    "from": "candidate@gmail.com",
    "to": "hr@yourcompany.com",
    "subject": "Application: Backend Engineer",
    "messageId": "<CAF3x@mail.gmail.com>",
    "reason": null,
    "createdAt": "2026-11-03T08:15:02.114Z"
  }
}

Verifying webhooks

Every request has a Sendas-Signature: t=…,v1=… header: an HMAC-SHA256 of "<t>.<raw body>" with your webhook’s secret. Check it before trusting the event.

import crypto from 'node:crypto'

// rawBody: the request body exactly as received, before JSON parsing
function verifySendas(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=')))
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false // older than 5 minutes
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
  return v1?.length === expected.length && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
}

Retries

Answer with any 2xx status within 10 seconds. Otherwise Sendas tries again after 1 minute, 5 minutes, 30 minutes, 2 hours and 8 hours, then gives up on that event. A retry keeps the same id, so skip ids you have already handled. After 50 failures in a row the webhook is switched off until you switch it back on. Redirects are not followed, and URLs that point to private or local addresses are refused.

Endpoints