Documentation

Domains

Register the domains you send from, get the DNS records to publish, and check their verification, bounce-path and click-tracking state.

A sending domain (a sending identity) is a domain SendRaven may put in the From line. Each one has a risk class. Transactional mail (password resets, receipts, replies) and marketing mail (campaigns, automations) go out from different subdomains, mail.example.com and news.example.com, each with its own SES configuration set. Mailbox providers score reputation on the From domain, so this split is what stops a campaign's complaint rate from sending your password resets to spam. Configuration sets alone only separate our own metrics.

The normal call is to give the domain you own, example.com, and let the API provision both subdomains. A send's from must then be on the verified identity whose risk class matches the message: team@mail.example.com for transactional mail, team@news.example.com for marketing. A new marketing identity is warmed up: its marketing mail is capped per day on a ramp that starts at its first send, so a large first campaign spreads across several days rather than arriving at mailbox providers all at once. Transactional mail is never capped. See Campaigns.

The domain object

FieldTypeDescription
idstringUnique identifier (UUID). Every path below takes this, not the domain name.
domainstringThe fully-qualified sending domain, lower-cased, for example mail.example.com.
risk_class"transactional" | "marketing"Which kind of mail this identity carries.
status"pending" | "verified" | "failed" | "temporary_failure"SES's DKIM verdict for the domain, with one exception. A record showing ok: true does not verify a domain on its own, and a verified domain stays verified while an optional record is unpublished. But a verified domain becomes failed when a check finds one of its DKIM records missing or pointing elsewhere, or when none of its DNS lookups has answered for 24 hours, even while SES still reports success. Mail is not sent from a failed domain.
configuration_setstringThe SES configuration set this identity's mail is sent through.
dns_recordsarray of objectThe records to publish, each with the result of the last live check. See below.
click_trackingobject | nullState of the link. hostname that carries click and open tracking on your own domain. null when click tracking has not been provisioned for this identity.
click_tracking.hostnamestringThe tracking hostname, link. followed by the sending domain.
click_tracking.status"pending" | "active" | "failed"pending until the CNAME resolves and a certificate has been issued for it; active once it has. Links are rewritten and opens are counted only while this is active.
click_tracking.active_atstring (ISO 8601) | nullWhen tracking first became active.
click_tracking.errorstring | nullWhy the hostname failed, set when status becomes failed. It is cleared when a later check finds the hostname pending or active, but not when the CNAME stops resolving, so it can still be present while status is pending.
mail_fromobject | nullWhether SES has adopted bounce. plus the sending domain as the Return-Path. null until the first check that reaches SES.
mail_from.domainstringThe bounce subdomain, for example bounce.mail.example.com.
mail_from.status"pending" | "active" | "failed"pending while SES polls for the MX record, active once adopted, failed when SES gave up.
mail_from.active_atstring (ISO 8601) | nullWhen the bounce path first became active.
verified_atstring (ISO 8601) | nullWhen a check first found the domain verified. It is set once and does not change on later checks, even if the domain stops being verified and verifies again. null until then.
created_atstring (ISO 8601)When the domain was added.

Each entry in dns_records:

FieldTypeDescription
kind"dkim" | "spf" | "dmarc" | "mail_from_mx" | "mail_from_spf" | "inbound_mx" | "click_tracking"What the record is for.
namestringThe hostname to publish the record at.
type"CNAME" | "TXT" | "MX"The record type.
valuestringThe value to publish.
priorityinteger | nullMX priority. null on CNAME and TXT records.
observedstring | nullThe first answer the last DNS check saw at name, or null if nothing was there or no check has run yet.
okbooleanWhether the last check found the expected value. DMARC matches any record starting v=DMARC1 and the SPF record any value that includes amazonses.com, so a record you have extended with your own reporting addresses or includes still passes. When a check cannot reach a resolver, the previous ok and observed are kept.
optionalbooleantrue for records that turn on a feature rather than being needed to send.

A new domain returns these records:

KindTypeNameRequired
dkimCNAMEThree records, token._domainkey. plus the domain, pointing at token.dkim.amazonses.comYes. SES verifies the domain from these.
mail_from_mxMXbounce. plus the domain, priority 10, pointing at feedback-smtp.region.amazonses.comFor the bounce path. Mail is delivered without it on an amazonses.com Return-Path, but SPF then aligns with Amazon's domain instead of yours.
mail_from_spfTXTbounce. plus the domain, v=spf1 include:amazonses.com ~allSame as above.
dmarcTXT_dmarc. plus the domain, v=DMARC1; p=none; fo=1Recommended. p=none to start, because a stricter policy on a domain that has not sent yet can bounce legitimate mail while alignment settles. Tighten it once the domain has sent cleanly for a couple of weeks.
inbound_mxMXThe sending domain itself, priority 10, pointing at inbound-smtp.region.amazonaws.comOptional. Publish it if replies to this domain should arrive as inbound messages and threads.
click_trackingCNAMElink. plus the domainOptional. Puts click and open tracking on your own name. Present only when click tracking has been provisioned for the identity.

Publish the values the API returns rather than copying them from this page: the DKIM tokens are per domain, and the region and CNAME target depend on the account.

The mail_from bounce path is tracked beside verification, never as part of it. SES polls for the MX record for 72 hours after the domain is added and then marks it failed for good. If you publish the record after that, the next check that sees the MX resolve restarts SES's verification, and mail_from.status goes back to pending.

Click and open tracking are only ever served from your own link. hostname, never from a shared SendRaven or Amazon domain. An identity whose tracking hostname is not active sends its links unchanged and records no opens.

List domains

GET /v1/domains · requires domains:read

Returns every sending domain in the workspace, newest first, in the list envelope. This collection is not paged: limit and cursor are ignored, has_more is false and next_cursor is null. has_more becomes true only if the workspace reaches 500 identities, and every identity is still returned.

Example

curl https://api.sendraven.ai/v1/domains \
  -H "Authorization: Bearer $SENDRAVEN_API_KEY"

Response

200 OK

{
  "object": "list",
  "data": [
    {
      "id": "3f6d2b8e-8a1c-4a5e-9d2f-6b7c1e0a4d93",
      "domain": "mail.example.com",
      "risk_class": "transactional",
      "status": "verified",
      "configuration_set": "acme-transactional-example-com",
      "dns_records": [
        {
          "kind": "dkim",
          "name": "k7x2m4q9r1t5w8y3z6a0b2c4d6e8f0g1._domainkey.mail.example.com",
          "type": "CNAME",
          "value": "k7x2m4q9r1t5w8y3z6a0b2c4d6e8f0g1.dkim.amazonses.com",
          "priority": null,
          "observed": "k7x2m4q9r1t5w8y3z6a0b2c4d6e8f0g1.dkim.amazonses.com",
          "ok": true,
          "optional": false
        },
        {
          "kind": "mail_from_mx",
          "name": "bounce.mail.example.com",
          "type": "MX",
          "value": "feedback-smtp.us-west-2.amazonses.com",
          "priority": 10,
          "observed": "feedback-smtp.us-west-2.amazonses.com",
          "ok": true,
          "optional": false
        },
        {
          "kind": "mail_from_spf",
          "name": "bounce.mail.example.com",
          "type": "TXT",
          "value": "v=spf1 include:amazonses.com ~all",
          "priority": null,
          "observed": "v=spf1 include:amazonses.com ~all",
          "ok": true,
          "optional": false
        },
        {
          "kind": "dmarc",
          "name": "_dmarc.mail.example.com",
          "type": "TXT",
          "value": "v=DMARC1; p=none; fo=1",
          "priority": null,
          "observed": "v=DMARC1; p=none; fo=1",
          "ok": true,
          "optional": false
        },
        {
          "kind": "inbound_mx",
          "name": "mail.example.com",
          "type": "MX",
          "value": "inbound-smtp.us-west-2.amazonaws.com",
          "priority": 10,
          "observed": null,
          "ok": false,
          "optional": true
        },
        {
          "kind": "click_tracking",
          "name": "link.mail.example.com",
          "type": "CNAME",
          "value": "cname-target.example.net",
          "priority": null,
          "observed": "cname-target.example.net",
          "ok": true,
          "optional": true
        }
      ],
      "click_tracking": {
        "hostname": "link.mail.example.com",
        "status": "active",
        "active_at": "2026-09-14T09:30:00.000Z",
        "error": null
      },
      "mail_from": {
        "domain": "bounce.mail.example.com",
        "status": "active",
        "active_at": "2026-09-14T09:12:00.000Z"
      },
      "verified_at": "2026-09-15T12:00:00.000Z",
      "created_at": "2026-09-14T09:00:00.000Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

The example shows one DKIM record for brevity; a real domain has three.

Add a domain

POST /v1/domains · requires domains:write

Registers a sending domain with SES and returns the DNS records to publish.

Without risk_class, which is almost always what you want, the API provisions two identities beneath a root: mail. for transactional mail and news. for marketing. The root is the domain you give, except that one leading mail. or news. is stripped first, so mail.example.com and example.com both produce mail.example.com and news.example.com, never mail.mail.example.com. It is stripped only when what remains is still a domain someone can register, as the Public Suffix List defines it. A registrable domain whose first label happens to be mail or news, such as mail.co.uk (co.uk is a public suffix), is itself the root, and both identities go beneath it: mail.mail.co.uk and news.mail.co.uk. Those are the only names under a domain its owner controls; stripping would have meant news.co.uk, which is somebody else's domain. Adding either identity again files under the same root and creates nothing new.

You giveRootIdentities
example.com, mail.example.com or news.example.comexample.commail.example.com, news.example.com
mail.example.co.ukexample.co.ukmail.example.co.uk, news.example.co.uk
eu.example.comeu.example.commail.eu.example.com, news.eu.example.com
mail.co.uk, mail.mail.co.uk or news.mail.co.ukmail.co.ukmail.mail.co.uk, news.mail.co.uk
news.ionews.iomail.news.io, news.news.io
mail.news.example.comnews.example.commail.news.example.com, news.news.example.com
co.uk, github.iorefused, 422none
.example.com, -acme.comrefused, 422none

A public suffix on its own, such as co.uk, or github.io from the list's private section of platforms that hand out subdomains, is refused with 422, and so is a name that is not a hostname (an empty label, or a label that starts or ends with a hyphen), with or without risk_class. With risk_class, exactly one identity is created, on the domain as given (lower-cased, nothing stripped), with that risk class: to send from mail.co.uk itself, pass it with risk_class.

The call is safe to repeat. A domain that already exists in the workspace is returned as it is, with no change, and the response is still 201. That lookup is by domain name alone, so passing risk_class for a domain that already exists returns the existing identity whatever its risk class.

Plans limit the number of domains, counted by root: mail.example.com and news.example.com are one domain, and so are mail.example.co.uk and news.example.co.uk.

Each newly created identity comes back as a domain object with status: "pending". Its dns_records have not been checked yet, so observed is null and ok is false, and mail_from is null until the first check. The background monitor checks pending domains about once a minute; call Verify a domain to check straight away once the records are published.

Body

FieldTypeRequiredDefaultDescription
domainstringYesnoneA bare domain, at least 3 characters, letters, digits, dots and hyphens, ending in a TLD of two or more letters. No scheme, path or @.
risk_class"transactional" | "marketing"NononeLeave out to provision both mail. and news. subdomains. Pass it only to register one identity, on the domain as given.

Example

curl https://api.sendraven.ai/v1/domains \
  -X POST \
  -H "Authorization: Bearer $SENDRAVEN_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "domain": "example.com" }'

Response

201 Created

The list envelope, with one domain object per identity in data, transactional first. has_more is always false and next_cursor always null.

{
  "object": "list",
  "data": [
    {
      "id": "3f6d2b8e-8a1c-4a5e-9d2f-6b7c1e0a4d93",
      "domain": "mail.example.com",
      "risk_class": "transactional",
      "status": "pending",
      "configuration_set": "acme-transactional-example-com",
      "dns_records": [
        {
          "kind": "dkim",
          "name": "k7x2m4q9r1t5w8y3z6a0b2c4d6e8f0g1._domainkey.mail.example.com",
          "type": "CNAME",
          "value": "k7x2m4q9r1t5w8y3z6a0b2c4d6e8f0g1.dkim.amazonses.com",
          "priority": null,
          "observed": null,
          "ok": false,
          "optional": false
        },
        {
          "kind": "mail_from_mx",
          "name": "bounce.mail.example.com",
          "type": "MX",
          "value": "feedback-smtp.us-west-2.amazonses.com",
          "priority": 10,
          "observed": null,
          "ok": false,
          "optional": false
        },
        {
          "kind": "mail_from_spf",
          "name": "bounce.mail.example.com",
          "type": "TXT",
          "value": "v=spf1 include:amazonses.com ~all",
          "priority": null,
          "observed": null,
          "ok": false,
          "optional": false
        },
        {
          "kind": "dmarc",
          "name": "_dmarc.mail.example.com",
          "type": "TXT",
          "value": "v=DMARC1; p=none; fo=1",
          "priority": null,
          "observed": null,
          "ok": false,
          "optional": false
        },
        {
          "kind": "inbound_mx",
          "name": "mail.example.com",
          "type": "MX",
          "value": "inbound-smtp.us-west-2.amazonaws.com",
          "priority": 10,
          "observed": null,
          "ok": false,
          "optional": true
        },
        {
          "kind": "click_tracking",
          "name": "link.mail.example.com",
          "type": "CNAME",
          "value": "cname-target.example.net",
          "priority": null,
          "observed": null,
          "ok": false,
          "optional": true
        }
      ],
      "click_tracking": {
        "hostname": "link.mail.example.com",
        "status": "pending",
        "active_at": null,
        "error": null
      },
      "mail_from": null,
      "verified_at": null,
      "created_at": "2026-09-15T12:00:00.000Z"
    },
    {
      "id": "b1e4c7d0-2f5a-4b8c-9e1d-3a6f9c2e5b70",
      "domain": "news.example.com",
      "risk_class": "marketing",
      "status": "pending",
      "configuration_set": "acme-marketing-example-com",
      "dns_records": [
        {
          "kind": "dkim",
          "name": "p3n8v1c6h2j9l4s7u0e5i8o1a3d6f9g2._domainkey.news.example.com",
          "type": "CNAME",
          "value": "p3n8v1c6h2j9l4s7u0e5i8o1a3d6f9g2.dkim.amazonses.com",
          "priority": null,
          "observed": null,
          "ok": false,
          "optional": false
        }
      ],
      "click_tracking": {
        "hostname": "link.news.example.com",
        "status": "pending",
        "active_at": null,
        "error": null
      },
      "mail_from": null,
      "verified_at": null,
      "created_at": "2026-09-15T12:00:00.000Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

The second identity is abbreviated to one record; it returns the same set as the first, on news.example.com. Registering the tracking hostname is best effort: if it fails at creation, click_tracking is null, there is no click_tracking record, and the next check retries it.

The identities are created one after the other. If the second fails with an unexpected error (500), the first has already been stored; repeating the call returns it and creates the missing one.

Errors

StatusTypeWhen
402plan_limit_reachedThe workspace's plan allows no more sending domains. Retrying will not help; upgrading will. This is the same type a send past the plan's allowance answers.
404not_foundThe key's workspace no longer exists.
422invalid_requestdomain is missing or not a bare domain, or risk_class is not one of the two values. details lists the problems.
422invalid_requestdomain is a public suffix (co.uk, com.au) rather than a domain registered under one (domain: co.uk is a public suffix, …), or is not a domain name at all, such as .example.com or -acme.com (domain: .example.com is not a domain name, …).

Retrieve a domain

GET /v1/domains/{id} · requires domains:read

Returns one domain object. The stored state is returned as last checked; to re-check DNS now, use Verify a domain.

Path parameters

ParameterDescription
idThe domain's id. Passing the domain name returns 404 with a message naming the id to use.

Example

curl https://api.sendraven.ai/v1/domains/3f6d2b8e-8a1c-4a5e-9d2f-6b7c1e0a4d93 \
  -H "Authorization: Bearer $SENDRAVEN_API_KEY"

Response

200 OK

A domain object, the same shape as an entry in List domains.

Errors

StatusTypeWhen
404not_foundNo domain with this id in the workspace. When the value is the name of one of your domains, the message says so and gives its id.

Delete a domain

DELETE /v1/domains/{id} · requires domains:write

Removes the sending identity from the workspace, releases its link. tracking hostname, and deletes the domain at SES together with the configuration set that was created for it. Its DKIM keys stop being valid for sending, so adding the same domain again later issues new DKIM tokens and the DNS records have to be published again. Mail already sent keeps its history. Scheduled mail that was due to go out from this identity fails at send time rather than going out from somewhere else.

SES holds one identity per domain for the whole account, so when another sending identity, in this workspace or any other, still uses exactly the same domain, the SES identity is kept and only this workspace's record is removed. A configuration set is deleted only when it belongs to this identity alone; identities added before 8 Sep 2026 share a configuration set per workspace, which is kept. A domain or configuration set that SES no longer has is treated as already deleted.

If SES refuses to delete the domain for any other reason, nothing is removed and the call answers 502 ses_error. The domain is still listed and the call can be repeated. Removing the configuration set is best effort: if SES refuses only that, the domain is still removed and the unused configuration set stays in SES, where it sends and verifies nothing.

Deleting one identity leaves its sibling alone: removing news.example.com does not remove mail.example.com.

Path parameters

ParameterDescription
idThe domain's id.

Example

curl https://api.sendraven.ai/v1/domains/3f6d2b8e-8a1c-4a5e-9d2f-6b7c1e0a4d93 \
  -X DELETE \
  -H "Authorization: Bearer $SENDRAVEN_API_KEY"

Response

200 OK

{
  "id": "3f6d2b8e-8a1c-4a5e-9d2f-6b7c1e0a4d93",
  "deleted": true
}

Errors

StatusTypeWhen
404not_foundNo domain with this id in the workspace. When the value is the name of one of your domains, the message says so and gives its id.
502ses_errorSES refused to delete the domain or its configuration set. Nothing was removed; try again.

Verify a domain

POST /v1/domains/{id}/verify · requires domains:write

Checks the domain now instead of waiting for the background monitor, which looks at pending and temporary_failure domains about once a minute and verified ones about every ten minutes. A domain that failed because its DKIM records stopped resolving is also re-checked about every ten minutes, and goes back to verified once they resolve again. A domain that SES itself failed is not re-checked automatically, so this call is the only way to move it on after fixing its records. It takes no body.

A check does four things. It looks up every record in dns_records in live DNS and stores what it saw. It asks SES for its DKIM verdict, which is what sets status, unless SES reports success for DKIM records the lookup could not find (see status above). It reconciles the bounce path: if SES had given up on mail_from and the MX record now resolves, it restarts SES's verification. And it checks the click-tracking hostname, registering one first if the identity does not have one yet.

A resolver that times out or fails does not flip a record to missing: the record keeps its previous result. If SES cannot be reached, status stays as it was.

Do not reuse an Idempotency-Key across verify calls: a replay returns the stored result of the earlier check instead of checking again.

Path parameters

ParameterDescription
idThe domain's id.

Example

curl https://api.sendraven.ai/v1/domains/3f6d2b8e-8a1c-4a5e-9d2f-6b7c1e0a4d93/verify \
  -X POST \
  -H "Authorization: Bearer $SENDRAVEN_API_KEY"

Response

200 OK

The domain object as stored after the check, exactly what Retrieve a domain returns afterwards. Its status is this check's verdict.

Errors

StatusTypeWhen
404not_foundNo domain with this id in the workspace. When the value is the name of one of your domains, the message says so and gives its id.

On this page