API reference

The Rykon Mail API

A REST API over HTTPS. Requests and responses are JSON, every request is authenticated with an API key, and every error has the same shape. The reference below is drawn from the same table that routes each request, so it lists exactly what the API answers.

Base URL: https://app.rykonmail.com/api/v1

Send your key as Authorization: Bearer rmk_live_.... Each operation names the scope its key must hold. Read Authentication, Errors and Rate limits once; they apply to everything here. The whole API is also published as OpenAPI 3.1.

Authenticated request
curl https://app.rykonmail.com/api/v1/domains \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Error
403
{
  "error": "forbidden",
  "message": "this key does not hold audience:write"
}

2 endpoints

Emails

Send transactional email from your application: receipts, password resets, alerts.

POST/emails

Send an email

Sends one transactional email: a message your application sends one person because of something they did, such as a receipt, a sign-in link or an order update. It carries no unsubscribe link, so the rules are strict. An address that bounced or marked your mail as spam is never sent to. Someone who unsubscribed may still get account, security, receipt and order mail, but never a notification, and never a message that reads as selling; that is refused with the words that gave it away. Refusals answer 409 and are listed in GET /emails. The from address must be on one of your domains that is set up for sending (400 if the domain is not yours, 409 if it is not ready, 409 if sending is stopped for the account). One address gets at most 20 messages a day, and the account has an hourly limit by plan; either answers 429. Give text, html or both; without text, a plain text part is made from the HTML. Send an idempotencyKey to make retries safe: the same key returns the first message with "repeated": true instead of sending again. The message body is not kept, only who, when, the subject and the outcome. Needs a key holding mail:send.

Needsmail:sendReturns200 OK

Body

fromstring (email)required
The sender. Its domain must be one of yours, verified and set up for sending.
fromNamestringoptional
A display name for the sender, up to 120 characters.
tostring (email)required
The one recipient.
replyTostring (email)optional
Where replies go.
subjectstringrequired
1 to 300 characters.
textstringoptional
The plain text part, up to 200,000 characters. Give text, html or both.
htmlstringoptional
The HTML part, up to 500,000 characters.
category'account' | 'security' | 'receipt' | 'order' | 'notification'required
What kind of message this is. Only account, security, receipt and order mail may reach someone who unsubscribed.
idempotencyKeystringoptional
Up to 200 characters. A retry with the same key returns the first message and sends nothing.
POST/emails
curl -X POST https://app.rykonmail.com/api/v1/emails \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "orders@northwind-local.test",
    "fromName": "Northwind Traders",
    "to": "jamie.chen@example.com",
    "replyTo": "help@northwind-local.test",
    "subject": "Your receipt for order 10482",
    "text": "Thanks for your order.\n\nOrder 10482: 2 x Linen apron, $48.00.\nTotal paid: $48.00.",
    "html": "<p>Thanks for your order.</p><p>Order 10482: 2 x Linen apron, $48.00.<br>Total paid: <strong>$48.00</strong>.</p>",
    "category": "receipt",
    "idempotencyKey": "order-10482-receipt"
  }'
Response
200 OK
{
  "id": "682e5ffd-1528-4b9e-9b81-65903d3d9593",
  "category": "receipt",
  "from": "orders@northwind-local.test",
  "to": "jamie.chen@example.com",
  "subject": "Your receipt for order 10482",
  "state": "sent",
  "messageId": "simulated-fa61a1bd-2389-4b00-ab0c-63ac54867b0c",
  "error": null,
  "createdAt": "2026-10-30T06:18:05.177Z"
}
GET/emails

List sent emails

The 100 most recent transactional messages, newest first, including the ones that were refused and why. With them: how many were sent in the last hour and day, how many were refused today, the account's hourly limit and the most one address may get in a day. Bodies are never kept, so they are not here. Needs sending:read.

Needssending:readReturns200 OK

Query parameters

tostring (email)optional
Only messages to this address.
state'sent' | 'refused' | 'failed'optional
Only messages in this state.
GET/emails
curl "https://app.rykonmail.com/api/v1/emails?to=jamie.chen%40example.com" \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "messages": [
    {
      "id": "ba525624-1079-436d-9525-88eb1b013547",
      "category": "order",
      "from": "orders@northwind-local.test",
      "to": "jamie.chen@example.com",
      "subject": "20% off everything this weekend",
      "state": "refused",
      "messageId": null,
      "error": "jamie.chen@example.com unsubscribed and this reads as marketing (% off, shop now); transactional mail must not sell",
      "createdAt": "2026-10-30T06:18:05.232Z"
    },
    {
      "id": "682e5ffd-1528-4b9e-9b81-65903d3d9593",
      "category": "receipt",
      "from": "orders@northwind-local.test",
      "to": "jamie.chen@example.com",
      "subject": "Your receipt for order 10482",
      "state": "sent",
      "messageId": "simulated-fa61a1bd-2389-4b00-ab0c-63ac54867b0c",
      "error": null,
      "createdAt": "2026-10-30T06:18:05.177Z"
    }
  ],
  "lastHour": 1,
  "lastDay": 27,
  "refusedToday": 25,
  "hourlyLimit": 2000,
  "perRecipientPerDay": 20
}

12 endpoints

Contacts

The people in your audience, with their consent and the topics they chose.

GET/contacts

List contacts

Contacts, newest first, with the account's totals by status (the totals count everyone, not just this page). Needs audience:read.

Needsaudience:readReturns200 OK

Query parameters

status'pending' | 'subscribed' | 'unsubscribed' | 'bounced' | 'complained'optional
Only contacts with this status.
tagstringoptional
Only contacts with this tag.
searchstringoptional
Part of an email address or a name; case does not matter.
limitintegeroptional
1 to 500; 100 if left out.
offsetintegeroptional
How many to skip, for the next page; 0 if left out.
GET/contacts
curl "https://app.rykonmail.com/api/v1/contacts?search=jamie&limit=2" \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "contacts": [
    {
      "id": "00410db1-2799-4dd4-afef-11f95befa5af",
      "email": "jamie.chen@example.com",
      "name": "Jamie Chen",
      "status": "subscribed",
      "tags": [
        "customer",
        "vip"
      ],
      "consent_source": "api:Checkout opt-in",
      "consent_at": "2026-10-30T06:13:46.762Z",
      "confirmed_at": "2026-10-30T06:13:46.762Z",
      "unsubscribed_at": null,
      "created_at": "2026-10-30T06:13:46.762Z"
    }
  ],
  "totals": {
    "subscribed": 3,
    "pending": 7,
    "unsubscribed": 1
  }
}
POST/contacts

Add a contact

Adds someone to your audience, with the consent they gave recorded word for word: what they agreed to, where, and optionally from which IP address. Answers 201 for a new contact. If the address is already subscribed (or pending, when you ask for double opt-in again), the contact is filled in rather than replaced: a name if given, new tags and attributes merged in, nothing taken away; that answers 200 with "created": false. With doubleOptIn the person is sent a confirmation email and stays pending until they use its link, which expires in 7 days; without it they are subscribed at once and any automation that starts on subscribing starts. Two things only the person can do: an address that unsubscribed or asked to be erased comes back only through their own confirmation, so it answers 409 unless doubleOptIn is true; and an address that bounced or marked your mail as spam can never be added again (409). Needs audience:write.

Needsaudience:writeReturns201 Created

Body

emailstring (email)required
Up to 320 characters; stored in lower case.
namestringoptional
Up to 200 characters.
tagsstring[]optional
Up to 20 tags of up to 40 characters each.
attributesobjectoptional
Up to 50 fields of your own: keys up to 60 characters, values a string (up to 500), number, boolean or null.
consentobjectrequired
What the person agreed to: text (10 to 2,000 characters, in the words they saw, required), source (where they agreed, up to 120 characters; "API" if left out) and ip (optional).
doubleOptInbooleanoptional
Send a confirmation email and keep the contact pending until it is used. false if left out.
POST/contacts
curl -X POST https://app.rykonmail.com/api/v1/contacts \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "jamie.chen@example.com",
    "name": "Jamie Chen",
    "tags": [
      "customer",
      "newsletter"
    ],
    "attributes": {
      "plan": "pro",
      "city": "Austin"
    },
    "consent": {
      "text": "Yes, send me product news and offers. I can unsubscribe at any time.",
      "source": "Checkout opt-in",
      "ip": "203.0.113.24"
    }
  }'
Response
201 Created
{
  "id": "00410db1-2799-4dd4-afef-11f95befa5af",
  "email": "jamie.chen@example.com",
  "name": "Jamie Chen",
  "status": "subscribed",
  "tags": [
    "customer",
    "newsletter"
  ],
  "attributes": {
    "city": "Austin",
    "plan": "pro"
  },
  "consent_source": "api:Checkout opt-in",
  "consent_at": "2026-10-30T06:13:46.762Z",
  "confirmed_at": "2026-10-30T06:13:46.762Z",
  "unsubscribed_at": null,
  "created_at": "2026-10-30T06:13:46.762Z",
  "created": true
}
POST/contacts/import/preview

Preview a CSV import

Reads a CSV file without importing anything: its columns, what each would become (guessed when you send no mapping), the first rows, and how many people are new, already on the list, or suppressed and so would be skipped, with the first 50 invalid addresses and duplicates. The file can be up to 5 MB and 50,000 rows. A mapping entry is email, name, first_name, last_name, tags, ignore, or field:<key> for a field of your own. Needs audience:write.

Needsaudience:writeReturns200 OK

Body

csvstringrequired
The file's contents, with a header row.
mappingstring[]optional
What each column holds, one entry per column. Guessed from the headers if left out.
POST/contacts/import/preview
curl -X POST https://app.rykonmail.com/api/v1/contacts/import/preview \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "csv": "email,first_name,last_name,company\nava.patel@example.com,Ava,Patel,Acme\nnoah.kim@example.com,Noah,Kim,Globex\nnot-an-address,Bad,Row,\nava.patel@example.com,Ava,Patel,Acme\n"
  }'
Response
200 OK
{
  "headers": [
    "email",
    "first_name"
  ],
  "mapping": [
    "email",
    "first_name"
  ],
  "sample": [
    [
      "ava.patel@example.com",
      "Ava"
    ],
    [
      "noah.kim@example.com",
      "Noah"
    ]
  ],
  "total": 4,
  "valid": 2,
  "new": 2,
  "existing": 0,
  "suppressed": 0,
  "invalid": [
    {
      "line": 4,
      "value": "not-an-address"
    }
  ],
  "invalidCount": 1,
  "duplicates": [
    {
      "line": 5,
      "email": "ava.patel@example.com"
    }
  ],
  "duplicateCount": 1,
  "needsEmailColumn": false
}
POST/contacts/import

Import contacts from CSV

Imports a CSV file. An upload is never consent, so you declare where these people came from: basis "subscribed" is your own statement that they agreed, recorded word for word on every contact created; "unconfirmed" keeps them pending, and pending contacts are never sent marketing. People already on the list keep their status: an import only adds tags and fills empty fields. Unsubscribed, bounced and complained addresses are skipped. The mapping must have one entry per column and exactly one email column. Every import can be undone. Same limits as the preview. Needs audience:write.

Needsaudience:writeReturns201 Created

Body

csvstringrequired
The file's contents, with a header row.
mappingstring[]required
What each column holds: email, name, first_name, last_name, tags, ignore or field:<key>.
basis'subscribed' | 'unconfirmed'required
Whether these people agreed to hear from you, by your declaration, or still need to confirm.
consentTextstringrequired
20 to 1,000 characters: where these people came from and how they agreed.
declarationbooleanrequired
Must be true: you confirm the declaration above.
fileNamestringoptional
The file's name, for the import's record.
tagsstring[]optional
Up to 10 tags to add to everyone in the file: lower case letters, numbers and dashes.
POST/contacts/import
curl -X POST https://app.rykonmail.com/api/v1/contacts/import \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "csv": "email,first_name,last_name,company\nava.patel@example.com,Ava,Patel,Acme\nnoah.kim@example.com,Noah,Kim,Globex\nnot-an-address,Bad,Row,\nava.patel@example.com,Ava,Patel,Acme\n",
    "fileName": "webinar-signups.csv",
    "mapping": [
      "email",
      "first_name",
      "last_name",
      "field:company"
    ],
    "basis": "unconfirmed",
    "consentText": "People who registered for our October webinar and ticked the box to hear from us.",
    "declaration": true,
    "tags": [
      "webinar"
    ]
  }'
Response
201 Created
{
  "id": "7ba4f9fa-30c1-4d1a-807e-0d8c3f057d6b",
  "rows": 4,
  "created": 2,
  "updated": 0,
  "skippedInvalid": 1,
  "skippedDuplicate": 1,
  "skippedSuppressed": 0,
  "basis": "unconfirmed"
}
GET/contacts/imports

List imports

The 50 most recent imports, newest first, with their counts, their declaration and whether they were undone. Needs audience:read.

Needsaudience:readReturns200 OK
GET/contacts/imports
curl https://app.rykonmail.com/api/v1/contacts/imports \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "imports": [
    {
      "id": "7ba4f9fa-30c1-4d1a-807e-0d8c3f057d6b",
      "file_name": "webinar-signups.csv",
      "basis": "unconfirmed",
      "consent_text": "People who registered for our October webinar and ticked the box to hear from us.",
      "tags": [
        "webinar"
      ],
      "rows": 4,
      "created": 2,
      "updated": 0,
      "skipped_invalid": 1,
      "skipped_duplicate": 1,
      "skipped_suppressed": 0,
      "created_by": "key sa1pteyz44qbm",
      "created_at": "2026-10-30T06:13:57.624Z",
      "undone_at": null,
      "undone_by": null,
      "undo_kept": null
    }
  ]
}
POST/contacts/imports/{id}/undo

Undo an import

Removes the people an import created who have not been sent anything since and have not signed up again by themselves, and takes off the tags it added to people who were already there. "kept" counts the people it created who stay. An import is undone once; a second try answers 409. Needs audience:write.

Needsaudience:writeReturns200 OK

Path parameters

iduuidrequired
The import's id.
POST/contacts/imports/{id}/undo
curl -X POST https://app.rykonmail.com/api/v1/contacts/imports/00410db1-2799-4dd4-afef-11f95befa5af/undo \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "removed": 2,
  "kept": 0
}
GET/contacts/{id}

Get a contact

One contact, with the consent on record (its text, source, time and IP address) and its 100 most recent events. Needs audience:read.

Needsaudience:readReturns200 OK

Path parameters

iduuidrequired
The contact's id.
GET/contacts/{id}
curl https://app.rykonmail.com/api/v1/contacts/00410db1-2799-4dd4-afef-11f95befa5af \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "id": "00410db1-2799-4dd4-afef-11f95befa5af",
  "email": "jamie.chen@example.com",
  "name": "Jamie Chen",
  "status": "subscribed",
  "tags": [
    "customer",
    "vip"
  ],
  "attributes": {
    "plan": "business"
  },
  "consent_source": "api:Checkout opt-in",
  "consent_text": "Yes, send me product news and offers. I can unsubscribe at any time.",
  "consent_at": "2026-10-30T06:13:46.762Z",
  "consent_ip": "203.0.113.24",
  "confirmed_at": "2026-10-30T06:13:46.762Z",
  "unsubscribed_at": null,
  "created_at": "2026-10-30T06:13:46.762Z",
  "events": [
    {
      "kind": "updated",
      "detail": {
        "by": "key sa1pteyz44qbm",
        "via": "api"
      },
      "created_at": "2026-10-30T06:13:46.890Z"
    },
    {
      "kind": "updated",
      "detail": {
        "by": "key sa1pteyz44qbm",
        "via": "api"
      },
      "created_at": "2026-10-30T06:13:46.833Z"
    }
  ]
}
PATCH/contacts/{id}

Update a contact

Changes a contact's name, tags and attributes. It never changes status: only the person's consent and unsubscribing do that. Attributes are merged in, and an attribute set to null is removed. Fields other than these are refused with 400. Needs audience:write.

Needsaudience:writeReturns200 OK

Path parameters

iduuidrequired
The contact's id.

Body

namestring | nulloptional
Up to 200 characters; null clears it.
addTagsstring[]optional
Up to 20 tags to add.
removeTagsstring[]optional
Up to 20 tags to remove.
attributesobjectoptional
Fields to set: a string (up to 500), number or boolean; null removes the field.
PATCH/contacts/{id}
curl -X PATCH https://app.rykonmail.com/api/v1/contacts/00410db1-2799-4dd4-afef-11f95befa5af \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Jamie Chen",
    "addTags": [
      "vip"
    ],
    "removeTags": [
      "newsletter"
    ],
    "attributes": {
      "plan": "business",
      "city": null
    }
  }'
Response
200 OK
{
  "id": "00410db1-2799-4dd4-afef-11f95befa5af",
  "email": "jamie.chen@example.com",
  "name": "Jamie Chen",
  "status": "subscribed",
  "tags": [
    "customer",
    "vip"
  ],
  "attributes": {
    "plan": "business"
  },
  "consent_source": "api:Checkout opt-in",
  "consent_at": "2026-10-30T06:13:46.762Z",
  "confirmed_at": "2026-10-30T06:13:46.762Z",
  "unsubscribed_at": null,
  "created_at": "2026-10-30T06:13:46.762Z"
}
POST/contacts/{id}/unsubscribe

Unsubscribe a contact

Unsubscribes someone on their behalf, for example when they ask you by email. They are added to your suppression list and taken out of any automation they are in. Only their own confirmation can bring them back. Needs audience:write.

Needsaudience:writeReturns200 OK

Path parameters

iduuidrequired
The contact's id.
POST/contacts/{id}/unsubscribe
curl -X POST https://app.rykonmail.com/api/v1/contacts/00410db1-2799-4dd4-afef-11f95befa5af/unsubscribe \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "id": "00410db1-2799-4dd4-afef-11f95befa5af",
  "status": "unsubscribed"
}
DELETE/contacts/{id}

Erase a contact

Erasure, for a request to be forgotten: the contact and their history are deleted. Their address stays on your suppression list so they are never mailed again by mistake; adding them back takes their own confirmation (POST /contacts with doubleOptIn). Needs audience:write.

Needsaudience:writeReturns200 OK

Path parameters

iduuidrequired
The contact's id.
DELETE/contacts/{id}
curl -X DELETE https://app.rykonmail.com/api/v1/contacts/00410db1-2799-4dd4-afef-11f95befa5af \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "erased": true
}
GET/contacts/{id}/topics

List a contact's topics

Every topic on the account, with when this person opted out of it and how, or null if they still receive it. Needs audience:read.

Needsaudience:readReturns200 OK

Path parameters

iduuidrequired
The contact's id.
GET/contacts/{id}/topics
curl https://app.rykonmail.com/api/v1/contacts/4d3e3999-dcc6-4f7f-898c-e7ab1e971c3e/topics \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "topics": [
    {
      "id": "4d3e3999-dcc6-4f7f-898c-e7ab1e971c3e",
      "name": "Offers 88153",
      "opted_out_at": null,
      "source": null
    },
    {
      "id": "ffbf4d82-d5fe-401a-b7c1-aea54f759542",
      "name": "Offers 76520",
      "opted_out_at": null,
      "source": null
    }
  ]
}
PUT/contacts/{id}/topics/{topicId}

Set a contact's topic choice

Records a choice the person made about one topic: whether they receive it. Use it only on their request; the contact's history keeps who recorded it. Unsubscribing still stops everything; a topic only narrows. Needs audience:write.

Needsaudience:writeReturns200 OK

Path parameters

iduuidrequired
The contact's id.
topicIduuidrequired
The topic's id.

Body

receivesbooleanrequired
true to receive the topic again, false to opt out of it.
PUT/contacts/{id}/topics/{topicId}
curl -X PUT https://app.rykonmail.com/api/v1/contacts/00410db1-2799-4dd4-afef-11f95befa5af/topics/TOPIC_ID \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "receives": false
  }'
Response
200 OK
{
  "receives": false
}

4 endpoints

Topics

What people can subscribe to, so each one hears only about what they asked for.

GET/topics

List topics

The account's topics, oldest first, with how many people opted out of each and how many campaigns use it. Needs audience:read.

Needsaudience:readReturns200 OK
GET/topics
curl https://app.rykonmail.com/api/v1/topics \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "topics": [
    {
      "id": "bea753b0-6132-4bc2-bc2a-18ff86ed0079",
      "name": "Product updates",
      "description": "New features and fixes, about once a month.",
      "created_at": "2026-10-30T06:13:45.895Z",
      "opted_out": 0,
      "campaigns": 0
    }
  ]
}
POST/topics

Create a topic

Adds a kind of email people can opt out of on its own, such as product updates or offers. Everyone subscribed receives a topic until they opt out of it; a campaign that names a topic leaves out those who did. Names are unique on the account (400 if taken), and an account can have 30 topics (400 beyond that). Needs audience:write.

Needsaudience:writeReturns201 Created

Body

namestringrequired
1 to 60 characters.
descriptionstringoptional
Up to 200 characters, shown to people choosing what they receive.
POST/topics
curl -X POST https://app.rykonmail.com/api/v1/topics \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Product updates",
    "description": "New features and changes, about once a month."
  }'
Response
201 Created
{
  "id": "bea753b0-6132-4bc2-bc2a-18ff86ed0079",
  "name": "Product updates",
  "description": "New features and changes, about once a month.",
  "created_at": "2026-10-30T06:13:45.895Z",
  "opted_out": 0,
  "campaigns": 0
}
PATCH/topics/{id}

Update a topic

Renames a topic or changes its description; send only what changes. A name already used answers 400. Needs audience:write.

Needsaudience:writeReturns200 OK

Path parameters

iduuidrequired
The topic's id.

Body

namestringoptional
1 to 60 characters.
descriptionstringoptional
Up to 200 characters.
PATCH/topics/{id}
curl -X PATCH https://app.rykonmail.com/api/v1/topics/bea753b0-6132-4bc2-bc2a-18ff86ed0079 \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "New features and fixes, about once a month."
  }'
Response
200 OK
{
  "id": "bea753b0-6132-4bc2-bc2a-18ff86ed0079",
  "name": "Product updates",
  "description": "New features and fixes, about once a month.",
  "created_at": "2026-10-30T06:13:45.895Z",
  "opted_out": 0,
  "campaigns": 0
}
DELETE/topics/{id}

Archive a topic

Archives a topic. It is never deleted, because past campaigns and people's choices still point at it; draft campaigns that named it no longer do. Needs audience:write.

Needsaudience:writeReturns200 OK

Path parameters

iduuidrequired
The topic's id.
DELETE/topics/{id}
curl -X DELETE https://app.rykonmail.com/api/v1/topics/bea753b0-6132-4bc2-bc2a-18ff86ed0079 \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "archived": true
}

5 endpoints

Forms

Sign-up forms and where they are live.

GET/forms

List forms

The account's sign-up forms, newest first, with how many contacts joined through each and the five sites it was most recently seen on. siteOrigin is where hosted forms live (/f/<public_id>). Needs audience:read.

Needsaudience:readReturns200 OK
GET/forms
curl https://app.rykonmail.com/api/v1/forms \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "forms": [
    {
      "id": "a5f0dd2d-bfc1-4247-84fa-6a8ff0dd0740",
      "public_id": "9QAqDsJMl79DfFKY",
      "name": "API docs example",
      "heading": "Get our monthly letter",
      "description": "New products and studio notes, about once a month.",
      "button_label": "Sign me up",
      "consent_text": "I agree to receive emails from us. I can unsubscribe at any time with the link in every email.",
      "ask_name": true,
      "double_opt_in": true,
      "success_message": "Thanks. Check your inbox to confirm.",
      "redirect_url": null,
      "reply_to": null,
      "tags": [
        "newsletter"
      ],
      "active": false,
      "allowed_sites": [
        "example.com"
      ],
      "created_at": "2026-10-30T06:14:07.200Z",
      "updated_at": "2026-10-30T06:14:07.475Z",
      "contacts": 0,
      "placements": []
    }
  ],
  "siteOrigin": "http://localhost:4320"
}
POST/forms

Create a form

Makes a sign-up form. People who submit it become contacts with the form's consent text recorded; with double opt-in (the default) they are subscribed only once they confirm. Creating a live form beyond your plan's live form limit answers 402; create it with active false, or switch another off. Needs audience:write.

Needsaudience:writeReturns201 Created

Body

namestringrequired
Your name for the form, 1 to 120 characters.
headingstringoptional
Up to 160 characters; "Stay in touch" if left out.
descriptionstringoptional
Up to 600 characters.
buttonLabelstringoptional
Up to 40 characters; "Subscribe" if left out.
consentTextstringoptional
What the person agrees to, 10 to 1,000 characters, recorded word for word on every contact. A sensible default if left out.
askNamebooleanoptional
Ask for a name as well as an address. true if left out.
doubleOptInbooleanoptional
Subscribe people only once they use the link mailed to them. true if left out.
successMessagestringoptional
Shown after submitting, up to 300 characters.
redirectUrlstring (url) | nulloptional
A page to send people to after submitting, instead of the message.
replyTostring (email) | nulloptional
Where replies to the confirmation email go.
tagsstring[]optional
Up to 10 tags given to everyone who signs up: lower case letters, numbers and dashes.
activebooleanoptional
Whether the form takes sign-ups. true if left out.
allowedSitesstring[]optional
Up to 20 host names the form may be shown on, like yourcompany.com; empty for anywhere. "www." is dropped.
POST/forms
curl -X POST https://app.rykonmail.com/api/v1/forms \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "API docs example",
    "heading": "Get our monthly letter",
    "description": "New products and studio notes, about once a month.",
    "tags": [
      "newsletter"
    ],
    "allowedSites": [
      "example.com"
    ],
    "active": false
  }'
Response
201 Created
{
  "id": "a5f0dd2d-bfc1-4247-84fa-6a8ff0dd0740",
  "public_id": "9QAqDsJMl79DfFKY",
  "name": "API docs example",
  "heading": "Get our monthly letter",
  "description": "New products and studio notes, about once a month.",
  "button_label": "Subscribe",
  "consent_text": "I agree to receive emails from us. I can unsubscribe at any time with the link in every email.",
  "ask_name": true,
  "double_opt_in": true,
  "success_message": "Thanks. Check your inbox to confirm.",
  "redirect_url": null,
  "reply_to": null,
  "tags": [
    "newsletter"
  ],
  "active": false,
  "allowed_sites": [
    "example.com"
  ],
  "created_at": "2026-10-30T06:14:07.200Z",
  "updated_at": "2026-10-30T06:14:07.200Z"
}
PATCH/forms/{id}

Update a form

Changes a form; send only what changes. Switching a form on counts against your plan's live form limit and answers 402 when there is no room; editing or switching off never does. Needs audience:write.

Needsaudience:writeReturns200 OK

Path parameters

iduuidrequired
The form's id.

Body

namestringoptional
Your name for the form, 1 to 120 characters.
headingstringoptional
Up to 160 characters; "Stay in touch" if left out.
descriptionstringoptional
Up to 600 characters.
buttonLabelstringoptional
Up to 40 characters; "Subscribe" if left out.
consentTextstringoptional
What the person agrees to, 10 to 1,000 characters, recorded word for word on every contact. A sensible default if left out.
askNamebooleanoptional
Ask for a name as well as an address. true if left out.
doubleOptInbooleanoptional
Subscribe people only once they use the link mailed to them. true if left out.
successMessagestringoptional
Shown after submitting, up to 300 characters.
redirectUrlstring (url) | nulloptional
A page to send people to after submitting, instead of the message.
replyTostring (email) | nulloptional
Where replies to the confirmation email go.
tagsstring[]optional
Up to 10 tags given to everyone who signs up: lower case letters, numbers and dashes.
activebooleanoptional
Whether the form takes sign-ups. true if left out.
allowedSitesstring[]optional
Up to 20 host names the form may be shown on, like yourcompany.com; empty for anywhere. "www." is dropped.
PATCH/forms/{id}
curl -X PATCH https://app.rykonmail.com/api/v1/forms/a5f0dd2d-bfc1-4247-84fa-6a8ff0dd0740 \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "buttonLabel": "Sign me up",
    "successMessage": "Thanks. Check your inbox to confirm."
  }'
Response
200 OK
{
  "id": "a5f0dd2d-bfc1-4247-84fa-6a8ff0dd0740",
  "public_id": "9QAqDsJMl79DfFKY",
  "name": "API docs example",
  "heading": "Get our monthly letter",
  "description": "New products and studio notes, about once a month.",
  "button_label": "Sign me up",
  "consent_text": "I agree to receive emails from us. I can unsubscribe at any time with the link in every email.",
  "ask_name": true,
  "double_opt_in": true,
  "success_message": "Thanks. Check your inbox to confirm.",
  "redirect_url": null,
  "reply_to": null,
  "tags": [
    "newsletter"
  ],
  "active": false,
  "allowed_sites": [
    "example.com"
  ],
  "created_at": "2026-10-30T06:14:07.200Z",
  "updated_at": "2026-10-30T06:14:07.475Z"
}
POST/forms/{id}/check-install

Check a form is on a page

Fetches a page of your site and looks for this form in it: placed with the script, a button that opens it, your own form connected to it, an iframe, or a link. Says how it was found and whether the script line is missing, or why it was not found. Only public pages can be fetched. Needs audience:read.

Needsaudience:readReturns200 OK

Path parameters

iduuidrequired
The form's id.

Body

urlstringrequired
The page to look at, up to 2,000 characters; https:// is assumed if left off.
POST/forms/{id}/check-install
curl -X POST https://app.rykonmail.com/api/v1/forms/a5f0dd2d-bfc1-4247-84fa-6a8ff0dd0740/check-install \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/"
  }'
Response
200 OK
{
  "found": false,
  "url": "https://example.com/",
  "reason": "The form is not in this page as it was sent. If your site builds pages in the browser, it may still show once the page h..."
}
DELETE/forms/{id}

Delete a form

Deletes a sign-up form. Its hosted link and every embed stop answering; the contacts it brought in stay in your audience and keep the record of what they agreed to, which is stored with them. Refused with 409 while an automation starts from the form, naming those automations: deleting the form would turn them into automations that start for every sign-up, so change or delete them first. 404 if the form is not in your account. Needs audience:write.

Needsaudience:writeReturns200 OK

Path parameters

iduuidrequired
The form's id.
DELETE/forms/{id}
curl -X DELETE https://app.rykonmail.com/api/v1/forms/a5f0dd2d-bfc1-4247-84fa-6a8ff0dd0740 \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "deleted": true
}

19 endpoints

Campaigns

Create, test, schedule and measure campaigns.

GET/campaigns

List campaigns

Every campaign, newest first, with its state, whether its last check passed, and how many recipients it has and how many were sent. postalAddress is the account's, which every campaign carries. Needs audience:read.

Needsaudience:readReturns200 OK
GET/campaigns
curl https://app.rykonmail.com/api/v1/campaigns \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "campaigns": [
    {
      "id": "9e210b02-8326-4122-a449-2196ccd8cc38",
      "name": "API docs example",
      "subject": "Our autumn collection is here",
      "preheader": "Twelve new pieces, made in small batches.",
      "from_name": "Northwind Traders",
      "from_local": "news",
      "domain_id": "965a5cd1-455b-4c7d-8ae3-f68caf0c7993",
      "domain": "northwind-local.test",
      "reply_to": "hello@northwind-local.test",
      "body": "",
      "audience_tags": [
        "vip"
      ],
      "segment": null,
      "topic_id": "bea753b0-6132-4bc2-bc2a-18ff86ed0079",
      "experiment": null,
      "experiment_result": null,
      "brief": {},
      "updated_by": "key sa1pteyz44qbm",
      "state": "sent",
      "state_reason": null,
      "scheduled_at": "2026-10-30T08:15:19.116Z",
      "started_at": "2026-10-30T06:15:20.212Z",
      "finished_at": "2026-10-30T06:15:20.411Z",
      "created_at": "2026-10-30T06:14:34.949Z",
      "updated_at": "2026-10-30T06:15:20.411Z",
      "kind": "marketing",
      "validation_ok": true,
      "validation_blocking": 0,
      "recipients": 0,
      "sent": 0
    }
  ],
  "postalAddress": "12 Studio Lane, Austin, TX 78701"
}
POST/campaigns

Create a campaign

Creates a draft. Its design starts from a template, from an email's HTML you send, or from a starter design, in your brand look unless you ask for the original. A domainId that is not one of yours is left empty. Set the topic, segment, subject test and brief afterwards with PATCH /campaigns/:id. Needs audience:write.

Needsaudience:writeReturns201 Created

Body

namestringrequired
Your name for the campaign, 1 to 120 characters.
subjectstringoptional
Up to 200 characters. From the imported HTML's title if left out.
preheaderstringoptional
The preview line, up to 200 characters.
fromNamestringoptional
Up to 100 characters.
fromLocalstringoptional
The part of the from address before the @; "news" if left out.
domainIduuid | nulloptional
The sending domain.
replyTostring (email) | nulloptional
Where replies go.
audienceTagsstring[]optional
Up to 20 tags; the campaign goes to subscribed contacts with any of them, or everyone if empty.
templateIdstringoptional
A built in template ("builtin:...") or one of your own, as the starting design.
look'brand' | 'original'optional
Start in your brand look ("brand", the default) or the template's own.
htmlstringoptional
An existing email's HTML, up to 900,000 characters, read into the design (see POST /campaigns/import-html).
bodystringoptional
Plain body text in the older body language, up to 100,000 characters; leave it out to start from a design.
POST/campaigns
curl -X POST https://app.rykonmail.com/api/v1/campaigns \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "API docs example",
    "subject": "Our autumn collection is here",
    "preheader": "Twelve new pieces, made in small batches.",
    "fromName": "Northwind Traders",
    "fromLocal": "news",
    "domainId": "965a5cd1-455b-4c7d-8ae3-f68caf0c7993",
    "replyTo": "hello@northwind-local.test",
    "audienceTags": [
      "vip"
    ]
  }'
Response
201 Created
{
  "id": "9e210b02-8326-4122-a449-2196ccd8cc38",
  "name": "API docs example",
  "subject": "Our autumn collection is here",
  "preheader": "Twelve new pieces, made in small batches.",
  "from_name": "Northwind Traders",
  "from_local": "news",
  "domain_id": "965a5cd1-455b-4c7d-8ae3-f68caf0c7993",
  "domain": "northwind-local.test",
  "reply_to": "hello@northwind-local.test",
  "body": "",
  "audience_tags": [
    "vip"
  ],
  "segment": null,
  "topic_id": null,
  "experiment": null,
  "experiment_result": null,
  "brief": {},
  "updated_by": null,
  "state": "draft",
  "state_reason": null,
  "scheduled_at": null,
  "started_at": null,
  "finished_at": null,
  "created_at": "2026-10-30T06:14:34.949Z",
  "updated_at": "2026-10-30T06:14:34.949Z"
}
POST/campaigns/import-html

Read an email's HTML

Reads an existing email's HTML into a design without creating anything, and says what came across and what was left out, so you can check it before starting a campaign from it (send the same html to POST /campaigns). Up to 900,000 characters. Needs audience:write.

Needsaudience:writeReturns200 OK

Body

htmlstringrequired
The email's HTML.
POST/campaigns/import-html
curl -X POST https://app.rykonmail.com/api/v1/campaigns/import-html \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<html><head><title>Autumn sale</title></head><body><h1>Autumn sale</h1><p>Hi there, everything in the shop is 20% off this week.</p><a href=\"https://northwind-local.test/sale\" style=\"background:#0f766e;color:#fff;padding:12px 20px\">Shop the sale</a></body></html>"
  }'
Response
200 OK
{
  "design": {
    "version": 1,
    "theme": {
      "background": "#f4f6fb",
      "content": "#ffffff",
      "text": "#2b3245",
      "heading": "#0b0d14",
      "accent": "#0f766e",
      "onAccent": "#ffffff",
      "muted": "#7a8296",
      "font": "system",
      "radius": 12
    },
    "blocks": [
      {
        "id": "b-mvukw77w-5",
        "paddingTop": 0,
        "paddingBottom": 16,
        "background": "",
        "hideOnMobile": false,
        "type": "heading",
        "text": "Autumn sale",
        "level": 1,
        "align": "left",
        "color": ""
      },
      {
        "id": "b-mvukw77w-6",
        "paddingTop": 0,
        "paddingBottom": 16,
        "background": "",
        "hideOnMobile": false,
        "type": "text",
        "text": "Hi there, everything in the shop is 20% off this week.",
        "align": "left",
        "size": 16,
        "color": ""
      }
    ],
    "footerNote": "",
    "plainText": ""
  },
  "title": "Autumn sale",
  "preheader": "",
  "counts": {
    "headings": 1,
    "paragraphs": 1,
    "images": 0,
    "buttons": 1
  },
  "notes": [
    "The layout is now the Studio’s own: check the order, and set colours and fonts in Email style."
  ]
}
GET/campaigns/{id}

Get a campaign

One campaign, with its recipients by state, opens and clicks, and how many people its audience reaches now. Needs audience:read.

Needsaudience:readReturns200 OK

Path parameters

iduuidrequired
The campaign's id.
GET/campaigns/{id}
curl https://app.rykonmail.com/api/v1/campaigns/9e210b02-8326-4122-a449-2196ccd8cc38 \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "id": "9e210b02-8326-4122-a449-2196ccd8cc38",
  "name": "API docs example",
  "subject": "Our autumn collection is here",
  "preheader": "Twelve new pieces, made in small batches.",
  "from_name": "Northwind Traders",
  "from_local": "news",
  "domain_id": "965a5cd1-455b-4c7d-8ae3-f68caf0c7993",
  "domain": "northwind-local.test",
  "reply_to": "hello@northwind-local.test",
  "body": "",
  "audience_tags": [
    "vip"
  ],
  "segment": null,
  "topic_id": "bea753b0-6132-4bc2-bc2a-18ff86ed0079",
  "experiment": null,
  "experiment_result": null,
  "brief": {},
  "updated_by": "key sa1pteyz44qbm",
  "state": "draft",
  "state_reason": null,
  "scheduled_at": null,
  "started_at": null,
  "finished_at": null,
  "created_at": "2026-10-30T06:14:34.949Z",
  "updated_at": "2026-10-30T06:14:45.827Z",
  "stats": {},
  "engagement": {
    "opened": 0,
    "clicked": 0
  },
  "audienceSize": 0
}
PATCH/campaigns/{id}

Update a campaign

Changes a draft; send only what changes. Anything but a draft answers 409 (cancel a scheduled campaign to edit it). A topicId that is not one of yours answers 400. The brief is merged field by field. Changing anything after approval means approving again before it can be scheduled. Needs audience:write.

Needsaudience:writeReturns200 OK

Path parameters

iduuidrequired
The campaign's id.

Body

namestringoptional
1 to 120 characters.
subjectstringoptional
Up to 200 characters.
preheaderstringoptional
Up to 200 characters.
fromNamestringoptional
Up to 100 characters.
fromLocalstringoptional
The part of the from address before the @.
domainIduuid | nulloptional
The sending domain; one that is not yours is left empty.
replyTostring (email) | nulloptional
Where replies go.
audienceTagsstring[]optional
Up to 20 tags.
segmentobject | nulloptional
Rules the audience must match: { match: "all" | "any", rules: [...] }, with rules on tags, join date, form, source, past campaigns, attributes and name.
topicIduuid | nulloptional
The topic it belongs to; people who opted out of it are left out.
experimentobject | nulloptional
A subject test: { variants: [{ key: "B", subject }], testShare, metric: "clicks" | "opens", windowHours, minPerVariant }.
briefobjectoptional
Notes for writing help: goal, audience, offer, action, tone, sourceUrl, sourceNotes. An empty value removes its field.
bodystringoptional
Plain body text in the older body language; it replaces the design.
PATCH/campaigns/{id}
curl -X PATCH https://app.rykonmail.com/api/v1/campaigns/9e210b02-8326-4122-a449-2196ccd8cc38 \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "subject": "Our autumn collection is here",
    "topicId": "bea753b0-6132-4bc2-bc2a-18ff86ed0079"
  }'
Response
200 OK
{
  "id": "9e210b02-8326-4122-a449-2196ccd8cc38",
  "name": "API docs example",
  "subject": "Our autumn collection is here",
  "preheader": "Twelve new pieces, made in small batches.",
  "from_name": "Northwind Traders",
  "from_local": "news",
  "domain_id": "965a5cd1-455b-4c7d-8ae3-f68caf0c7993",
  "domain": "northwind-local.test",
  "reply_to": "hello@northwind-local.test",
  "body": "",
  "audience_tags": [
    "vip"
  ],
  "segment": null,
  "topic_id": "bea753b0-6132-4bc2-bc2a-18ff86ed0079",
  "experiment": null,
  "experiment_result": null,
  "brief": {},
  "updated_by": "key sa1pteyz44qbm",
  "state": "draft",
  "state_reason": null,
  "scheduled_at": null,
  "started_at": null,
  "finished_at": null,
  "created_at": "2026-10-30T06:14:34.949Z",
  "updated_at": "2026-10-30T06:14:45.827Z"
}
DELETE/campaigns/{id}

Delete a campaign

Deletes a draft that never went out, with its versions and history. Anything that started sending is kept as a record and answers 409. Needs audience:write.

Needsaudience:writeReturns200 OK

Path parameters

iduuidrequired
The campaign's id.
DELETE/campaigns/{id}
curl -X DELETE https://app.rykonmail.com/api/v1/campaigns/9e210b02-8326-4122-a449-2196ccd8cc38 \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "deleted": true
}
GET/campaigns/{id}/studio

Get a campaign's design

The campaign's design (theme and blocks), whether its approval still holds, who its audience reaches and who is left out and why, the personal fields it uses, and its last check. Needs audience:read.

Needsaudience:readReturns200 OK

Path parameters

iduuidrequired
The campaign's id.
GET/campaigns/{id}/studio
curl https://app.rykonmail.com/api/v1/campaigns/b-mvukur45-1/studio \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "design": {
    "version": 1,
    "theme": {
      "background": "#f4f6fb",
      "content": "#ffffff",
      "text": "#2b3245",
      "heading": "#052e2b",
      "accent": "#0f766e",
      "onAccent": "#ffffff",
      "muted": "#7a8296",
      "font": "serif",
      "radius": 12
    },
    "blocks": [
      {
        "id": "b-mvukur45-1",
        "paddingTop": 0,
        "paddingBottom": 16,
        "background": "",
        "hideOnMobile": false,
        "type": "header",
        "logoSrc": "",
        "logoAlt": "Northwind Traders",
        "logoWidth": 140,
        "text": "Northwind Traders",
        "align": "center"
      },
      {
        "id": "b-mvukur45-2",
        "paddingTop": 0,
        "paddingBottom": 16,
        "background": "",
        "hideOnMobile": false,
        "type": "heading",
        "text": "A headline that says what is new",
        "level": 1,
        "align": "left",
        "color": ""
      }
    ],
    "footerNote": "",
    "plainText": ""
  },
  "approval": {
    "holds": false,
    "versionId": null,
    "number": null
  },
  "audience": {
    "matching": 1,
    "eligible": 0,
    "topic_opted_out": 1,
    "suppressed": 0,
    "unconfirmed": 0,
    "unsubscribed": 0,
    "unnamed": 0,
    "invalid": 0
  },
  "fields": [
    "first_name",
    "name"
  ],
  "validation": null,
  "updatedBy": "key sa1pteyz44qbm",
  "updatedAt": "2026-10-30T06:14:35.021Z"
}
PUT/campaigns/{id}/design

Save a campaign's design

Replaces a draft's design with the one sent, checked against the design schema (version, theme, blocks, footerNote, plainText; start from GET /campaigns/:id/studio). Anything but a draft answers 409. A design changed after approval must be approved again. A custom HTML block ({ type: "html", html }, at most 50 KB) is stored only as the sanitiser leaves it; the answer lists each one under html with the HTML kept and what was removed. Needs audience:write.

Needsaudience:writeReturns200 OK

Path parameters

iduuidrequired
The campaign's id.

Body

designobjectrequired
The whole design, as GET /campaigns/:id/studio returns it.
PUT/campaigns/{id}/design
curl -X PUT https://app.rykonmail.com/api/v1/campaigns/b-mvukur45-1/design \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "design": {
      "version": 1,
      "theme": {
        "background": "#f4f6fb",
        "content": "#ffffff",
        "text": "#2b3245",
        "heading": "#052e2b",
        "accent": "#0f766e",
        "onAccent": "#ffffff",
        "muted": "#7a8296",
        "font": "serif",
        "radius": 12
      },
      "blocks": [
        {
          "id": "b-mvukur45-1",
          "paddingTop": 0,
          "paddingBottom": 16,
          "background": "",
          "hideOnMobile": false,
          "type": "header",
          "logoSrc": "",
          "logoAlt": "Northwind Traders",
          "logoWidth": 140,
          "text": "Northwind Traders",
          "align": "center"
        },
        {
          "id": "b-mvukur45-2",
          "paddingTop": 0,
          "paddingBottom": 16,
          "background": "",
          "hideOnMobile": false,
          "type": "heading",
          "text": "Our autumn collection is here",
          "level": 1,
          "align": "left",
          "color": ""
        },
        {
          "id": "b-mvukur45-3",
          "paddingTop": 0,
          "paddingBottom": 16,
          "background": "",
          "hideOnMobile": false,
          "type": "text",
          "text": "Hi {{ name | there }},\n\nTwelve new pieces, made in small batches in our Austin studio.",
          "align": "left",
          "size": 16,
          "color": ""
        },
        {
          "id": "b-mvukur45-4",
          "paddingTop": 0,
          "paddingBottom": 16,
          "background": "",
          "hideOnMobile": false,
          "type": "button",
          "label": "See the collection",
          "href": "https://northwind-local.test/autumn",
          "align": "left",
          "style": "filled",
          "color": ""
        }
      ],
      "footerNote": "",
      "plainText": ""
    }
  }'
Response
200 OK
{
  "savedAt": "2026-10-30T06:15:03.020Z",
  "html": []
}
POST/campaigns/{id}/validate

Check a campaign

Runs the checks a campaign must pass before it can be approved: blocking problems (placeholder text, links that go nowhere, an empty audience and the like) and advice. With deep, every link and image is fetched too. Needs audience:read.

Needsaudience:readReturns200 OK

Path parameters

iduuidrequired
The campaign's id.

Body

deepbooleanoptional
Also fetch each link and image. false if left out.
POST/campaigns/{id}/validate
curl -X POST https://app.rykonmail.com/api/v1/campaigns/9e210b02-8326-4122-a449-2196ccd8cc38/validate \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "deep": false
  }'
Response
200 OK
{
  "report": {
    "ok": true,
    "blocking": [],
    "advisory": [
      {
        "code": "plan-campaigns",
        "area": "delivery",
        "message": "Without Campaigns, forms and automation you can send 461 more campaign emails in this 30 days; this one goes to 1.",
        "fix": "Turn it on in Billing to send without this limit."
      }
    ],
    "checkedAt": "2026-10-30T06:15:03.080Z"
  },
  "audience": {
    "matching": 1,
    "eligible": 1,
    "topic_opted_out": 0,
    "suppressed": 0,
    "unconfirmed": 0,
    "unsubscribed": 0,
    "unnamed": 0,
    "invalid": 0
  },
  "bytes": 3667,
  "fields": [
    "name"
  ]
}
GET/campaigns/{id}/render

Render a campaign

The message as a recipient would see it, as HTML or plain text (not JSON): for a sample person, one of your contacts, someone with no name (to see the fallbacks) or a very long name. The unsubscribe link is a placeholder. Needs audience:read.

Needsaudience:readReturns200 OK

Path parameters

iduuidrequired
The campaign's id.

Query parameters

format'html' | 'text'optional
html if left out.
as'sample' | 'noname' | 'long'optional
Who it is rendered for; sample if left out.
contactIduuidoptional
Render it for this contact instead.
GET/campaigns/{id}/render
curl "https://app.rykonmail.com/api/v1/campaigns/9e210b02-8326-4122-a449-2196ccd8cc38/render?format=text&as=sample" \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response, text/plain
200 OK
Northwind Traders

Our autumn collection is here

Hi Alex,

Twelve new pieces, made in small batches in our Austin studio.

See the collection: https://northwind-local.test/autumn

--
You are receiving this because you subscribed to Northwind Traders.
Northwind Traders, 12 Studio Lane, Austin, TX 78701
Unsubscribe: #unsubscribe
POST/campaigns/{id}/test

Send a test

Sends one copy to each address, exactly as a send would compile it, with "[Test]" before the subject and an unsubscribe link that goes nowhere. Copies go only to people on your account (400 for anyone else), 30 an hour (429 beyond that). The campaign needs a sending domain that is set up (409). Needs audience:write.

Needsaudience:writeReturns200 OK

Path parameters

iduuidrequired
The campaign's id.

Body

tostring[]required
1 to 5 addresses of people on your account.
POST/campaigns/{id}/test
curl -X POST https://app.rykonmail.com/api/v1/campaigns/9e210b02-8326-4122-a449-2196ccd8cc38/test \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": [
      "ui-test@rykonmail-local.test"
    ]
  }'
Response
200 OK
{
  "results": [
    {
      "to": "ui-test@rykonmail-local.test",
      "sent": true
    }
  ]
}
POST/campaigns/{id}/approve

Approve a campaign

Approves the campaign exactly as it stands and keeps that version. Every blocking check must pass (409 with the report otherwise), and only a draft can be approved. Change anything afterwards and it must be approved again. Needs audience:write.

Needsaudience:writeReturns200 OK

Path parameters

iduuidrequired
The campaign's id.
POST/campaigns/{id}/approve
curl -X POST https://app.rykonmail.com/api/v1/campaigns/9e210b02-8326-4122-a449-2196ccd8cc38/approve \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "approved": true,
  "version": 1,
  "versionId": "1468cf84-2fb5-4a40-97c0-6edfb1926491"
}
POST/campaigns/{id}/schedule

Schedule or send a campaign

Sends now, or at a time. Only a draft whose approval still covers it as it stands can be scheduled (409 otherwise). Without the Campaigns add-on a campaign must fit in what is left of the free allowance, and with it the list must fit the plan; either answers 402. Scheduling twice never sends twice. Needs audience:write.

Needsaudience:writeReturns200 OK

Path parameters

iduuidrequired
The campaign's id.

Body

atstring (date-time) | nulloptional
When to send; now if null or left out.
POST/campaigns/{id}/schedule
curl -X POST https://app.rykonmail.com/api/v1/campaigns/9e210b02-8326-4122-a449-2196ccd8cc38/schedule \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "at": "2026-10-30T08:15:19Z"
  }'
Response
200 OK
{
  "id": "9e210b02-8326-4122-a449-2196ccd8cc38",
  "name": "API docs example",
  "subject": "Our autumn collection is here",
  "preheader": "Twelve new pieces, made in small batches.",
  "from_name": "Northwind Traders",
  "from_local": "news",
  "domain_id": "965a5cd1-455b-4c7d-8ae3-f68caf0c7993",
  "domain": "northwind-local.test",
  "reply_to": "hello@northwind-local.test",
  "body": "",
  "audience_tags": [
    "vip"
  ],
  "segment": null,
  "topic_id": "bea753b0-6132-4bc2-bc2a-18ff86ed0079",
  "experiment": null,
  "experiment_result": null,
  "brief": {},
  "updated_by": "key sa1pteyz44qbm",
  "state": "scheduled",
  "state_reason": null,
  "scheduled_at": "2026-10-30T08:15:18.947Z",
  "started_at": null,
  "finished_at": null,
  "created_at": "2026-10-30T06:14:34.949Z",
  "updated_at": "2026-10-30T06:15:19.947Z"
}
POST/campaigns/{id}/pause

Pause a send

Stops a campaign part way: nothing still queued goes out; what was already handed to the sending service cannot be called back. "held" is how many copies wait. Only a campaign that is sending can be paused (409). Needs audience:write.

Needsaudience:writeReturns200 OK

Path parameters

iduuidrequired
The campaign's id.
POST/campaigns/{id}/pause
curl -X POST https://app.rykonmail.com/api/v1/campaigns/9e210b02-8326-4122-a449-2196ccd8cc38/pause \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "state": "paused",
  "held": 0
}
POST/campaigns/{id}/resume

Resume a send

Carries on sending a paused campaign from where it stopped. Only a paused campaign can be resumed (409). Needs audience:write.

Needsaudience:writeReturns200 OK

Path parameters

iduuidrequired
The campaign's id.
POST/campaigns/{id}/resume
curl -X POST https://app.rykonmail.com/api/v1/campaigns/9e210b02-8326-4122-a449-2196ccd8cc38/resume \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "state": "sending"
}
POST/campaigns/{id}/cancel

Cancel a campaign

A scheduled campaign that has not started goes back to being a draft; one that is sending or paused stops and is cancelled. Anything else answers 409. Needs audience:write.

Needsaudience:writeReturns200 OK

Path parameters

iduuidrequired
The campaign's id.
POST/campaigns/{id}/cancel
curl -X POST https://app.rykonmail.com/api/v1/campaigns/9e210b02-8326-4122-a449-2196ccd8cc38/cancel \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "id": "9e210b02-8326-4122-a449-2196ccd8cc38",
  "name": "API docs example",
  "subject": "Our autumn collection is here",
  "preheader": "Twelve new pieces, made in small batches.",
  "from_name": "Northwind Traders",
  "from_local": "news",
  "domain_id": "965a5cd1-455b-4c7d-8ae3-f68caf0c7993",
  "domain": "northwind-local.test",
  "reply_to": "hello@northwind-local.test",
  "body": "",
  "audience_tags": [
    "vip"
  ],
  "segment": null,
  "topic_id": "bea753b0-6132-4bc2-bc2a-18ff86ed0079",
  "experiment": null,
  "experiment_result": null,
  "brief": {},
  "updated_by": "key sa1pteyz44qbm",
  "state": "draft",
  "state_reason": null,
  "scheduled_at": null,
  "started_at": null,
  "finished_at": null,
  "created_at": "2026-10-30T06:14:34.949Z",
  "updated_at": "2026-10-30T06:15:20.041Z"
}
POST/campaigns/{id}/duplicate

Duplicate a campaign

A new draft with the same design, settings and audience, named "<name> (copy)", for the next send. Answers with its id. Needs audience:write.

Needsaudience:writeReturns201 Created

Path parameters

iduuidrequired
The campaign's id.
POST/campaigns/{id}/duplicate
curl -X POST https://app.rykonmail.com/api/v1/campaigns/bc95b28b-90e9-473c-93a4-bf8d79e4f3f6/duplicate \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
201 Created
{
  "id": "bc95b28b-90e9-473c-93a4-bf8d79e4f3f6"
}
GET/campaigns/{id}/report

Get a campaign's report

What happened to a campaign: each measure with its definition and rate, clicks by link, a timeline, the subject test's result, progress, and plain observations comparing it with your usual results. Needs audience:read.

Needsaudience:readReturns200 OK

Path parameters

iduuidrequired
The campaign's id.
GET/campaigns/{id}/report
curl https://app.rykonmail.com/api/v1/campaigns/9e210b02-8326-4122-a449-2196ccd8cc38/report \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "observations": [
    "Nothing has been delivered yet, so there is nothing to compare."
  ],
  "test": null,
  "metrics": [
    {
      "key": "recipients",
      "label": "Recipients",
      "value": 2,
      "rate": null,
      "of": null,
      "definition": "People who were eligible when sending started: subscribed, not suppressed, matching the audience."
    },
    {
      "key": "submitted",
      "label": "Sent",
      "value": 2,
      "rate": 1,
      "of": "recipients",
      "definition": "Messages accepted by our sending service. Not yet a delivery."
    }
  ],
  "links": [],
  "timeline": [],
  "progress": {
    "queued": 0,
    "skipped": 0,
    "failed": 0,
    "firstSent": "2026-10-30T03:34:46.270Z",
    "lastSent": "2026-10-30T03:34:46.274Z"
  }
}
GET/campaigns/{id}/versions

List a campaign's versions

The campaign's saved versions, newest first (up to 100), with who made each and which were approved. Needs audience:read.

Needsaudience:readReturns200 OK

Path parameters

iduuidrequired
The campaign's id.
GET/campaigns/{id}/versions
curl https://app.rykonmail.com/api/v1/campaigns/1468cf84-2fb5-4a40-97c0-6edfb1926491/versions \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "versions": [
    {
      "id": "1468cf84-2fb5-4a40-97c0-6edfb1926491",
      "number": 1,
      "source": "approval",
      "note": "approved",
      "subject": "Our autumn collection is here",
      "content_hash": "e1abd5b754f627e8d73e9727f1199fbab6a60d699c0dcf15dc79e57cd907aa7c",
      "created_by": "key sa1pteyz44qbm",
      "created_at": "2026-10-30T06:15:03.264Z",
      "approved": true
    }
  ]
}

8 endpoints

Automations

Journeys that run when someone subscribes.

GET/automations

List automations

The account's automations, newest first, with how many steps each has, how many people are in it now, how many have started it and how many emails it sent. Needs audience:read.

Needsaudience:readReturns200 OK
GET/automations
curl https://app.rykonmail.com/api/v1/automations \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "automations": [
    {
      "id": "77fcfb0a-d16e-4046-b3b1-cc1828553111",
      "name": "API docs example",
      "trigger_form_id": "a5f0dd2d-bfc1-4247-84fa-6a8ff0dd0740",
      "trigger_form": "API docs example",
      "trigger_tag": null,
      "active": false,
      "from_name": "Northwind Traders",
      "from_local": "hello",
      "domain_id": "965a5cd1-455b-4c7d-8ae3-f68caf0c7993",
      "domain": "northwind-local.test",
      "reply_to": null,
      "created_at": "2026-10-30T06:15:53.563Z",
      "updated_at": "2026-10-30T06:15:53.698Z",
      "emails": 3,
      "running": 0,
      "started": 0,
      "sent": 0
    }
  ]
}
POST/automations

Create an automation

Creates an automation, switched off, that starts with one empty welcome email sent as soon as someone subscribes, from your first domain that is set up for sending. Fill it in with PUT /automations/:id. Needs audience:write.

Needsaudience:writeReturns201 Created

Body

namestringrequired
1 to 120 characters.
POST/automations
curl -X POST https://app.rykonmail.com/api/v1/automations \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "API docs example"
  }'
Response
201 Created
{
  "id": "77fcfb0a-d16e-4046-b3b1-cc1828553111",
  "name": "API docs example",
  "trigger_form_id": null,
  "trigger_form": null,
  "trigger_tag": null,
  "active": false,
  "from_name": "",
  "from_local": "hello",
  "domain_id": "965a5cd1-455b-4c7d-8ae3-f68caf0c7993",
  "domain": "northwind-local.test",
  "reply_to": null,
  "created_at": "2026-10-30T06:15:53.563Z",
  "updated_at": "2026-10-30T06:15:53.563Z",
  "steps": [
    {
      "id": "73606f24-166c-446c-9b55-94278a975061",
      "position": 0,
      "kind": "email",
      "condition": "always",
      "condition_value": null,
      "tags_add": [],
      "tags_remove": [],
      "field_key": null,
      "field_value": null,
      "notify_to": null,
      "delay_minutes": 0,
      "subject": "",
      "preheader": "",
      "body": "",
      "sent": 0,
      "delivered": 0,
      "bounced": 0,
      "complained": 0,
      "failed": 0,
      "skipped": 0,
      "opened": 0,
      "clicked": 0
    }
  ],
  "runs": {}
}
GET/automations/{id}

Get an automation

One automation, with every step and its results (sent, delivered, opened, clicked, skipped and so on) and its people by state. Needs audience:read.

Needsaudience:readReturns200 OK

Path parameters

iduuidrequired
The automation's id.
GET/automations/{id}
curl https://app.rykonmail.com/api/v1/automations/77fcfb0a-d16e-4046-b3b1-cc1828553111 \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "id": "77fcfb0a-d16e-4046-b3b1-cc1828553111",
  "name": "API docs example",
  "trigger_form_id": "a5f0dd2d-bfc1-4247-84fa-6a8ff0dd0740",
  "trigger_form": "API docs example",
  "trigger_tag": null,
  "active": false,
  "from_name": "Northwind Traders",
  "from_local": "hello",
  "domain_id": "965a5cd1-455b-4c7d-8ae3-f68caf0c7993",
  "domain": "northwind-local.test",
  "reply_to": null,
  "created_at": "2026-10-30T06:15:53.563Z",
  "updated_at": "2026-10-30T06:15:53.698Z",
  "steps": [
    {
      "id": "73606f24-166c-446c-9b55-94278a975061",
      "position": 0,
      "kind": "email",
      "condition": "always",
      "condition_value": null,
      "tags_add": [],
      "tags_remove": [],
      "field_key": null,
      "field_value": null,
      "notify_to": null,
      "delay_minutes": 0,
      "subject": "Welcome to Northwind Traders",
      "preheader": "",
      "body": "Hi {{ name | there }},\n\nThanks for subscribing. Here is what to expect: one letter a month, never more.",
      "sent": 0,
      "delivered": 0,
      "bounced": 0,
      "complained": 0,
      "failed": 0,
      "skipped": 0,
      "opened": 0,
      "clicked": 0
    },
    {
      "id": "17b3a766-54af-4fd5-b54b-54f6a552cad1",
      "position": 1,
      "kind": "email",
      "condition": "not_opened",
      "condition_value": null,
      "tags_add": [],
      "tags_remove": [],
      "field_key": null,
      "field_value": null,
      "notify_to": null,
      "delay_minutes": 4320,
      "subject": "In case you missed it",
      "preheader": "",
      "body": "Hi {{ name | there }},\n\nA quick reminder of what is new this month.",
      "sent": 0,
      "delivered": 0,
      "bounced": 0,
      "complained": 0,
      "failed": 0,
      "skipped": 0,
      "opened": 0,
      "clicked": 0
    }
  ],
  "runs": {}
}
PUT/automations/{id}

Save an automation

Saves the settings and the whole sequence of steps; send them all. A step is an email, a handover to a person (notify), a tag change, a field change or the end, each after a wait, and may depend on whether the previous email was opened or clicked or on a tag. Send a step's id to keep it (and its history) when steps move. An email that has already been sent cannot be removed (409). A handover goes only to someone on the account or an address at one of its domains (400). The trigger form and the domain must be yours (400). People part way through carry on from the step they were going to do next. Needs audience:write.

Needsaudience:writeReturns200 OK

Path parameters

iduuidrequired
The automation's id.

Body

namestringrequired
1 to 120 characters.
triggerFormIduuid | nullrequired
Start for people who join through this form; null for any form.
triggerTagstring | nullrequired
Start only for people with this tag.
fromNamestringrequired
Up to 100 characters.
fromLocalstringrequired
The part of the from address before the @.
domainIduuid | nullrequired
The sending domain.
replyTostring (email) | nullrequired
Where replies go.
stepsobject[]required
1 to 20 steps: id, kind ("email", "notify", "tag", "field", "end"), condition ("always", "opened", "not_opened", "clicked", "not_clicked", "has_tag", "lacks_tag"), conditionValue, delayMinutes (required, up to 525,600), subject, preheader, body, notifyTo, tagsAdd, tagsRemove, fieldKey, fieldValue.
PUT/automations/{id}
curl -X PUT https://app.rykonmail.com/api/v1/automations/73606f24-166c-446c-9b55-94278a975061 \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "API docs example",
    "triggerFormId": "a5f0dd2d-bfc1-4247-84fa-6a8ff0dd0740",
    "triggerTag": null,
    "fromName": "Northwind Traders",
    "fromLocal": "hello",
    "domainId": "965a5cd1-455b-4c7d-8ae3-f68caf0c7993",
    "replyTo": null,
    "steps": [
      {
        "id": "73606f24-166c-446c-9b55-94278a975061",
        "kind": "email",
        "delayMinutes": 0,
        "subject": "Welcome to Northwind Traders",
        "body": "Hi {{ name | there }},\n\nThanks for subscribing. Here is what to expect: one letter a month, never more."
      },
      {
        "kind": "email",
        "condition": "not_opened",
        "delayMinutes": 4320,
        "subject": "In case you missed it",
        "body": "Hi {{ name | there }},\n\nA quick reminder of what is new this month."
      },
      {
        "kind": "tag",
        "delayMinutes": 0,
        "tagsAdd": [
          "onboarded"
        ]
      }
    ]
  }'
Response
200 OK
{
  "id": "77fcfb0a-d16e-4046-b3b1-cc1828553111",
  "name": "API docs example",
  "trigger_form_id": "a5f0dd2d-bfc1-4247-84fa-6a8ff0dd0740",
  "trigger_form": "API docs example",
  "trigger_tag": null,
  "active": false,
  "from_name": "Northwind Traders",
  "from_local": "hello",
  "domain_id": "965a5cd1-455b-4c7d-8ae3-f68caf0c7993",
  "domain": "northwind-local.test",
  "reply_to": null,
  "created_at": "2026-10-30T06:15:53.563Z",
  "updated_at": "2026-10-30T06:15:53.637Z",
  "steps": [
    {
      "id": "73606f24-166c-446c-9b55-94278a975061",
      "position": 0,
      "kind": "email",
      "condition": "always",
      "condition_value": null,
      "tags_add": [],
      "tags_remove": [],
      "field_key": null,
      "field_value": null,
      "notify_to": null,
      "delay_minutes": 0,
      "subject": "Welcome to Northwind Traders",
      "preheader": "",
      "body": "Hi {{ name | there }},\n\nThanks for subscribing. Here is what to expect: one letter a month, never more.",
      "sent": 0,
      "delivered": 0,
      "bounced": 0,
      "complained": 0,
      "failed": 0,
      "skipped": 0,
      "opened": 0,
      "clicked": 0
    },
    {
      "id": "17b3a766-54af-4fd5-b54b-54f6a552cad1",
      "position": 1,
      "kind": "email",
      "condition": "not_opened",
      "condition_value": null,
      "tags_add": [],
      "tags_remove": [],
      "field_key": null,
      "field_value": null,
      "notify_to": null,
      "delay_minutes": 4320,
      "subject": "In case you missed it",
      "preheader": "",
      "body": "Hi {{ name | there }},\n\nA quick reminder of what is new this month.",
      "sent": 0,
      "delivered": 0,
      "bounced": 0,
      "complained": 0,
      "failed": 0,
      "skipped": 0,
      "opened": 0,
      "clicked": 0
    }
  ],
  "runs": {}
}
DELETE/automations/{id}

Delete an automation

Deletes an automation nobody has been through. Once people have, its history is kept and this answers 409; pause it instead. Needs audience:write.

Needsaudience:writeReturns200 OK

Path parameters

iduuidrequired
The automation's id.
DELETE/automations/{id}
curl -X DELETE https://app.rykonmail.com/api/v1/automations/77fcfb0a-d16e-4046-b3b1-cc1828553111 \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "ok": true
}
POST/automations/{id}/activate

Switch an automation on

Switches it on for people who subscribe from now; those already on the list are not mailed. It needs a sending domain that is set up (409), the account's postal address (400), a subject and body on every email and someone to tell on every handover (400). Needs audience:write.

Needsaudience:writeReturns200 OK

Path parameters

iduuidrequired
The automation's id.
POST/automations/{id}/activate
curl -X POST https://app.rykonmail.com/api/v1/automations/77fcfb0a-d16e-4046-b3b1-cc1828553111/activate \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "id": "77fcfb0a-d16e-4046-b3b1-cc1828553111",
  "name": "API docs example",
  "trigger_form_id": "a5f0dd2d-bfc1-4247-84fa-6a8ff0dd0740",
  "trigger_form": "API docs example",
  "trigger_tag": null,
  "active": true,
  "from_name": "Northwind Traders",
  "from_local": "hello",
  "domain_id": "965a5cd1-455b-4c7d-8ae3-f68caf0c7993",
  "domain": "northwind-local.test",
  "reply_to": null,
  "created_at": "2026-10-30T06:15:53.563Z",
  "updated_at": "2026-10-30T06:15:53.681Z",
  "steps": [
    {
      "id": "73606f24-166c-446c-9b55-94278a975061",
      "position": 0,
      "kind": "email",
      "condition": "always",
      "condition_value": null,
      "tags_add": [],
      "tags_remove": [],
      "field_key": null,
      "field_value": null,
      "notify_to": null,
      "delay_minutes": 0,
      "subject": "Welcome to Northwind Traders",
      "preheader": "",
      "body": "Hi {{ name | there }},\n\nThanks for subscribing. Here is what to expect: one letter a month, never more.",
      "sent": 0,
      "delivered": 0,
      "bounced": 0,
      "complained": 0,
      "failed": 0,
      "skipped": 0,
      "opened": 0,
      "clicked": 0
    },
    {
      "id": "17b3a766-54af-4fd5-b54b-54f6a552cad1",
      "position": 1,
      "kind": "email",
      "condition": "not_opened",
      "condition_value": null,
      "tags_add": [],
      "tags_remove": [],
      "field_key": null,
      "field_value": null,
      "notify_to": null,
      "delay_minutes": 4320,
      "subject": "In case you missed it",
      "preheader": "",
      "body": "Hi {{ name | there }},\n\nA quick reminder of what is new this month.",
      "sent": 0,
      "delivered": 0,
      "bounced": 0,
      "complained": 0,
      "failed": 0,
      "skipped": 0,
      "opened": 0,
      "clicked": 0
    }
  ],
  "runs": {}
}
POST/automations/{id}/pause

Pause an automation

Nobody new starts, and nobody already in it is sent anything until it is switched on again. Needs audience:write.

Needsaudience:writeReturns200 OK

Path parameters

iduuidrequired
The automation's id.
POST/automations/{id}/pause
curl -X POST https://app.rykonmail.com/api/v1/automations/77fcfb0a-d16e-4046-b3b1-cc1828553111/pause \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "id": "77fcfb0a-d16e-4046-b3b1-cc1828553111",
  "name": "API docs example",
  "trigger_form_id": "a5f0dd2d-bfc1-4247-84fa-6a8ff0dd0740",
  "trigger_form": "API docs example",
  "trigger_tag": null,
  "active": false,
  "from_name": "Northwind Traders",
  "from_local": "hello",
  "domain_id": "965a5cd1-455b-4c7d-8ae3-f68caf0c7993",
  "domain": "northwind-local.test",
  "reply_to": null,
  "created_at": "2026-10-30T06:15:53.563Z",
  "updated_at": "2026-10-30T06:15:53.698Z",
  "steps": [
    {
      "id": "73606f24-166c-446c-9b55-94278a975061",
      "position": 0,
      "kind": "email",
      "condition": "always",
      "condition_value": null,
      "tags_add": [],
      "tags_remove": [],
      "field_key": null,
      "field_value": null,
      "notify_to": null,
      "delay_minutes": 0,
      "subject": "Welcome to Northwind Traders",
      "preheader": "",
      "body": "Hi {{ name | there }},\n\nThanks for subscribing. Here is what to expect: one letter a month, never more.",
      "sent": 0,
      "delivered": 0,
      "bounced": 0,
      "complained": 0,
      "failed": 0,
      "skipped": 0,
      "opened": 0,
      "clicked": 0
    },
    {
      "id": "17b3a766-54af-4fd5-b54b-54f6a552cad1",
      "position": 1,
      "kind": "email",
      "condition": "not_opened",
      "condition_value": null,
      "tags_add": [],
      "tags_remove": [],
      "field_key": null,
      "field_value": null,
      "notify_to": null,
      "delay_minutes": 4320,
      "subject": "In case you missed it",
      "preheader": "",
      "body": "Hi {{ name | there }},\n\nA quick reminder of what is new this month.",
      "sent": 0,
      "delivered": 0,
      "bounced": 0,
      "complained": 0,
      "failed": 0,
      "skipped": 0,
      "opened": 0,
      "clicked": 0
    }
  ],
  "runs": {}
}
GET/automations/{id}/runs

List an automation's people

Each person's progress, newest first (up to 200): where they are, when their next step is due, what each step did, or why they stopped. Needs audience:read.

Needsaudience:readReturns200 OK

Path parameters

iduuidrequired
The automation's id.

Query parameters

state'running' | 'completed' | 'exited' | 'failed'optional
Only people in this state.
GET/automations/{id}/runs
curl "https://app.rykonmail.com/api/v1/automations/d94cbb6d-481e-46e7-bd38-3fb48b6e643f/runs?state=completed" \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "runs": [
    {
      "id": "d94cbb6d-481e-46e7-bd38-3fb48b6e643f",
      "email": "riley.1793252140@reader-local.test",
      "name": "Riley Chen",
      "state": "completed",
      "next_position": 2,
      "next_at": "2026-10-29T05:42:17.604Z",
      "exit_reason": null,
      "waiting_reason": null,
      "started_at": "2026-10-29T05:36:05.795Z",
      "finished_at": "2026-10-29T05:37:17.637Z",
      "sends": [
        {
          "position": 0,
          "state": "sent",
          "sentAt": "2026-10-29T05:36:16.998971+00:00",
          "error": null
        },
        {
          "position": 1,
          "state": "skipped",
          "sentAt": "2026-10-29T05:36:47.308649+00:00",
          "error": "condition not met: not opened"
        }
      ]
    }
  ]
}

6 endpoints

Domains

Your sending domains and the DNS records that prove them.

GET/domains

List domains

The account's domains, newest first, with their state, whether their DNS has drifted, and whether ownership is proven. Needs domains:read.

Needsdomains:readReturns200 OK
GET/domains
curl https://app.rykonmail.com/api/v1/domains \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "domains": [
    {
      "id": "965a5cd1-455b-4c7d-8ae3-f68caf0c7993",
      "tenant_id": "7024c88d-abfb-4555-bd16-530a53d2c22f",
      "name": "northwind-local.test",
      "state": "active",
      "state_reason": null,
      "dns_mode": "external",
      "verified_at": null,
      "observed_at": null,
      "drifted": false,
      "drift_since": null,
      "created_at": "2026-10-29T05:14:24.823Z",
      "owned": true
    }
  ]
}
POST/domains

Add a domain

Adds a domain and starts setting it up; it answers 202 with the job, because setup waits on DNS. First publish the ownership record it returns: nothing is configured for the domain until that record is seen, and then the mail records to publish appear on GET /domains/:id. Retrying joins the setup already running. A domain already on your account or registered on the platform answers 409, as does one this platform runs on. Your plan sets how many domains you can have (409 beyond that). Needs domains:write.

Needsdomains:writeReturns202 Accepted

Body

domainstringrequired
The domain name, like yourcompany.com.
dnsMode'external' | 'delegated'optional
Who hosts the DNS; external (yours) if left out.
quotasobjectoptional
Optional limits for the domain: maxMailboxes, defaultQuotaMb, maxQuotaMb, domainQuotaMb.
POST/domains
curl -X POST https://app.rykonmail.com/api/v1/domains \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "domain": "api-docs-example.test"
  }'
Response
202 Accepted
{
  "id": "a463c960-1309-4174-b629-b03ff1d6ba1c",
  "domain": "api-docs-example.test",
  "state": "ownership_pending",
  "ownership": {
    "kind": "TXT",
    "name": "_rykonmail.api-docs-example.test",
    "value": "rykonmail-verification=fwvhweev823wa8j4tmq4fm6yzd5vm8ye",
    "purpose": "Proves this domain is yours. Nothing is set up until it is seen.",
    "verified": false,
    "verifiedAt": null
  },
  "job": {
    "id": "40b62db3-14f6-466d-905a-ee2d9218c38c",
    "state": "queued"
  },
  "next": "Publish the ownership record. Nothing is configured for this domain until it is seen, and then the mail records follow."
}
GET/domains/{id}

Get a domain

One domain: the ownership record, the records to publish and what was last seen of each, the records still outstanding, the sending identity, where its mailboxes connect, its recent setup jobs and a plain summary of where it stands. Needs domains:read.

Needsdomains:readReturns200 OK

Path parameters

iduuidrequired
The domain's id.
GET/domains/{id}
curl https://app.rykonmail.com/api/v1/domains/a463c960-1309-4174-b629-b03ff1d6ba1c \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "id": "a463c960-1309-4174-b629-b03ff1d6ba1c",
  "tenant_id": "7024c88d-abfb-4555-bd16-530a53d2c22f",
  "name": "api-docs-example.test",
  "state": "ownership_pending",
  "state_reason": null,
  "dns_mode": "external",
  "desired_records": [],
  "observed_records": [],
  "observed_at": null,
  "verified_at": null,
  "drift": null,
  "drift_since": null,
  "ownership_verified_at": null,
  "created_at": "2026-10-30T06:16:36.079Z",
  "ownership": {
    "kind": "TXT",
    "name": "_rykonmail.api-docs-example.test",
    "value": "rykonmail-verification=fwvhweev823wa8j4tmq4fm6yzd5vm8ye",
    "purpose": "Proves this domain is yours. Nothing is set up until it is seen.",
    "verified": false,
    "verifiedAt": null
  },
  "mailHost": null,
  "webmailUrl": "https://mail.rykonmail.com/",
  "sendingIdentity": null,
  "outstanding": [],
  "summary": "Waiting for the ownership record.",
  "jobs": [
    {
      "id": "40b62db3-14f6-466d-905a-ee2d9218c38c",
      "operation": "domain.provision",
      "state": "queued",
      "wait_reason": null,
      "failure_reason": null,
      "attempts": 0,
      "needs_operator": false,
      "created_at": "2026-10-30T06:16:36.079Z",
      "finished_at": null
    }
  ]
}
POST/domains/{id}/verify

Check a domain now

Checks the domain's DNS now instead of waiting for the next sweep, and answers 202 with the job. While setup is still running it is brought forward ("nudged"); setup that stopped is started again ("retried"), at most once a minute (409 sooner). Needs domains:write.

Needsdomains:writeReturns202 Accepted

Path parameters

iduuidrequired
The domain's id.
POST/domains/{id}/verify
curl -X POST https://app.rykonmail.com/api/v1/domains/40b62db3-14f6-466d-905a-ee2d9218c38c/verify \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
202 Accepted
{
  "job": {
    "id": "40b62db3-14f6-466d-905a-ee2d9218c38c"
  },
  "nudged": true
}
GET/domains/{id}/setup

Get a domain's DNS setup options

Who hosts the domain's DNS, whether a DNS provider is connected, and whether the automatic ways to publish the records (a Cloudflare token, Domain Connect) are open for it. It asks the domain's nameservers, so fetch it once rather than polling. Needs domains:read.

Needsdomains:readReturns200 OK

Path parameters

iduuidrequired
The domain's id.
GET/domains/{id}/setup
curl https://app.rykonmail.com/api/v1/domains/965a5cd1-455b-4c7d-8ae3-f68caf0c7993/setup \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "zone": "northwind-local.test",
  "nameservers": [],
  "provider": null,
  "recordsReady": false,
  "connection": null,
  "cloudflare": {
    "available": true,
    "reason": null
  },
  "domainConnect": {
    "available": false,
    "provider": null,
    "reason": "Domain Connect is off on simulated backends: it would publish records the simulation made up."
  },
  "simulated": true
}
DELETE/domains/{id}

Remove a domain

Removes a domain from your account and from every backend: the mail store, the sending identity and the relay, so mail to it no longer arrives here and nothing is sent from it. Because that cannot be undone, the body must name the domain in confirm (400 if it is missing, 409 if it does not match). Refused with 409 while the domain still has mailboxes (the message names up to five; delete them first, since each one holds mail) or while a campaign is scheduled, sending or paused from it. In the console only the owner or an admin may remove a domain. Needs domains:write.

Needsdomains:writeReturns200 OK

Path parameters

iduuidrequired
The domain's id.

Body

confirmstringrequired
The domain name, typed out.
DELETE/domains/{id}
curl -X DELETE https://app.rykonmail.com/api/v1/domains/965a5cd1-455b-4c7d-8ae3-f68caf0c7993 \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "confirm": "example.com"
  }'
Response
200 OK
{
  "removed": true,
  "name": "example.com"
}

4 endpoints

Mailboxes

Mailboxes on your domains.

GET/mailboxes

List mailboxes

The account's mailboxes, by address, with their quota, use, where they connect and which belong to an agent. Needs mailboxes:read.

Needsmailboxes:readReturns200 OK

Query parameters

domainIduuidoptional
Only mailboxes on this domain.
GET/mailboxes
curl "https://app.rykonmail.com/api/v1/mailboxes?domainId=965a5cd1-455b-4c7d-8ae3-f68caf0c7993" \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "mailboxes": [
    {
      "id": "7854885b-10e0-4dee-a7df-88351b9ae155",
      "address": "docs.example@northwind-local.test",
      "display_name": "API docs example",
      "quota_mb": 10240,
      "state": "active",
      "used_bytes": null,
      "message_count": null,
      "usage_at": null,
      "state_reason": null,
      "created_at": "2026-10-30T06:16:53.907Z",
      "domain": "northwind-local.test",
      "domain_id": "965a5cd1-455b-4c7d-8ae3-f68caf0c7993",
      "mail_host": "mail.rykontech.com",
      "webmail_url": "https://mail.rykonmail.com/",
      "agent_id": null
    }
  ]
}
POST/mailboxes

Create a mailbox

Creates a mailbox and answers 202 while it is made. It has no password until you issue one with POST /mailboxes/:id/password. The domain must be verified and its sending set up (409 until then, usually a minute or two after verifying). An address that exists answers 409, as does a quota above your plan's largest mailbox or a plan with no room for another. Needs mailboxes:write.

Needsmailboxes:writeReturns202 Accepted

Body

domainIduuidrequired
The domain it is on.
localPartstringrequired
The part before the @, up to 64 characters.
displayNamestringoptional
Up to 200 characters.
quotaMbintegeroptional
Size in MB, 64 or more; your plan's default if left out.
POST/mailboxes
curl -X POST https://app.rykonmail.com/api/v1/mailboxes \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "domainId": "965a5cd1-455b-4c7d-8ae3-f68caf0c7993",
    "localPart": "docs.example",
    "displayName": "API docs example"
  }'
Response
202 Accepted
{
  "id": "7854885b-10e0-4dee-a7df-88351b9ae155",
  "address": "docs.example@northwind-local.test",
  "state": "requested",
  "job": {
    "id": "493ab353-4e03-4b77-b451-a006ac0b2903"
  },
  "next": "The mailbox is being created. It has no password until one is issued through POST /v1/mailboxes/:id/password."
}
POST/mailboxes/{id}/password

Set a new mailbox password

Issues a new password and returns it in this response only: it is never stored and cannot be read back, so a lost one is replaced, not recovered. The old password stops working. Only an active mailbox can have one (409), and an agent's mailbox is managed through its agent (409). Needs mailboxes:write.

Needsmailboxes:writeReturns200 OK

Path parameters

iduuidrequired
The mailbox's id.
POST/mailboxes/{id}/password
curl -X POST https://app.rykonmail.com/api/v1/mailboxes/7854885b-10e0-4dee-a7df-88351b9ae155/password \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "address": "docs.example@northwind-local.test",
  "password": "w3we...",
  "shownOnce": "This password is shown once and is not stored. Issue a new one if it is lost."
}
DELETE/mailboxes/{id}

Delete a mailbox

Deletes a mailbox and every message in it. Because that cannot be undone, the body must name the address in confirm (400 if it is missing, 409 if it does not match). A mailbox that belongs to an agent, or that a shared address delivers to, answers 409 until that is changed. Needs mailboxes:write.

Needsmailboxes:writeReturns200 OK

Path parameters

iduuidrequired
The mailbox's id.

Body

confirmstringrequired
The mailbox's address, typed out.
DELETE/mailboxes/{id}
curl -X DELETE https://app.rykonmail.com/api/v1/mailboxes/7854885b-10e0-4dee-a7df-88351b9ae155 \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "confirm": "docs.example@northwind-local.test"
  }'
Response
200 OK
{
  "removed": true,
  "address": "docs.example@northwind-local.test"
}

4 endpoints

Aliases

Extra addresses that deliver to a mailbox.

GET/aliases

List shared addresses

The account's shared addresses, by address, with the mailboxes each delivers to. Needs mailboxes:read.

Needsmailboxes:readReturns200 OK
GET/aliases
curl https://app.rykonmail.com/api/v1/aliases \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "aliases": [
    {
      "id": "b9af2939-fc53-43cc-821b-07b180fae413",
      "address": "docs-sales@northwind-local.test",
      "domain_id": "965a5cd1-455b-4c7d-8ae3-f68caf0c7993",
      "created_at": "2026-10-30T06:17:15.298Z",
      "targets": [
        {
          "id": "7854885b-10e0-4dee-a7df-88351b9ae155",
          "address": "docs.example@northwind-local.test",
          "name": "API docs example"
        }
      ]
    }
  ]
}
POST/aliases

Create a shared address

Makes an address like sales@ that delivers to one or more of your own mailboxes, an agent's included. Only your own active mailboxes can receive (400 otherwise); forwarding outside the account is not offered. The domain must be verified (409). An address that is already a mailbox or a shared address answers 409, as does a plan with no room for another. Needs mailboxes:write.

Needsmailboxes:writeReturns201 Created

Body

domainIduuidrequired
The domain it is on.
localPartstringrequired
The part before the @, up to 64 characters.
targetsuuid[]required
1 to 50 ids of active mailboxes on the account.
POST/aliases
curl -X POST https://app.rykonmail.com/api/v1/aliases \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "domainId": "965a5cd1-455b-4c7d-8ae3-f68caf0c7993",
    "localPart": "docs-sales",
    "targets": [
      "7854885b-10e0-4dee-a7df-88351b9ae155",
      "14c2cc36-e349-419b-9bf6-4193fed78d1d"
    ]
  }'
Response
201 Created
{
  "id": "b9af2939-fc53-43cc-821b-07b180fae413",
  "address": "docs-sales@northwind-local.test",
  "domain_id": "965a5cd1-455b-4c7d-8ae3-f68caf0c7993",
  "created_at": "2026-10-30T06:17:15.298Z",
  "targets": [
    {
      "id": "14c2cc36-e349-419b-9bf6-4193fed78d1d",
      "address": "docs.agent@northwind-local.test",
      "name": "API docs example"
    },
    {
      "id": "7854885b-10e0-4dee-a7df-88351b9ae155",
      "address": "docs.example@northwind-local.test",
      "name": "API docs example"
    }
  ]
}
PUT/aliases/{id}

Change where a shared address delivers

Replaces the mailboxes a shared address delivers to. Only your own active mailboxes can receive (400 otherwise). Needs mailboxes:write.

Needsmailboxes:writeReturns200 OK

Path parameters

iduuidrequired
The shared address's id.

Body

targetsuuid[]required
1 to 50 ids of active mailboxes on the account.
PUT/aliases/{id}
curl -X PUT https://app.rykonmail.com/api/v1/aliases/b9af2939-fc53-43cc-821b-07b180fae413 \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "targets": [
      "7854885b-10e0-4dee-a7df-88351b9ae155"
    ]
  }'
Response
200 OK
{
  "id": "b9af2939-fc53-43cc-821b-07b180fae413",
  "address": "docs-sales@northwind-local.test",
  "domain_id": "965a5cd1-455b-4c7d-8ae3-f68caf0c7993",
  "created_at": "2026-10-30T06:17:15.298Z",
  "targets": [
    {
      "id": "7854885b-10e0-4dee-a7df-88351b9ae155",
      "address": "docs.example@northwind-local.test",
      "name": "API docs example"
    }
  ]
}
DELETE/aliases/{id}

Delete a shared address

Deletes a shared address; mail to it is no longer delivered. The mailboxes it delivered to are not touched. Needs mailboxes:write.

Needsmailboxes:writeReturns200 OK

Path parameters

iduuidrequired
The shared address's id.
DELETE/aliases/{id}
curl -X DELETE https://app.rykonmail.com/api/v1/aliases/b9af2939-fc53-43cc-821b-07b180fae413 \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "ok": true
}

8 endpoints

AI agents

Agents with mailboxes of their own, and the messages waiting for your approval.

GET/agents

List agents

The account's AI agents with their mailbox, what they may send and to whom, and what they did in the last 24 hours. Needs mailboxes:read.

Needsmailboxes:readReturns200 OK
GET/agents
curl https://app.rykonmail.com/api/v1/agents \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "agents": [
    {
      "id": "8ca997e9-6ff5-49bc-ba30-a0933f00012a",
      "name": "API docs example",
      "state": "active",
      "address": "docs.agent@northwind-local.test",
      "send_to": "listed",
      "allow_list": [
        "example.com"
      ],
      "daily_limit": 20,
      "approve_all": true,
      "created_at": "2026-10-30T06:16:53.930Z",
      "on_new_mail": "nothing",
      "notify_url": null,
      "notify_secret_set": false,
      "reply_instructions": "",
      "watched_at": null,
      "today": {
        "agent_id": "8ca997e9-6ff5-49bc-ba30-a0933f00012a",
        "sent": 1,
        "read": 0,
        "pending": 0
      }
    }
  ]
}
POST/agents

Create an agent

Gives an AI agent a mailbox of its own and answers while it is set up (state "provisioning"). Its mailbox counts against your plan. The domain must be verified and its sending set up (409 until then), and an address that exists answers 409. By default it may write only to people who wrote to it first, 50 a day; change that with PUT /agents/:id. Needs mailboxes:write.

Needsmailboxes:writeReturns201 Created

Body

namestringrequired
1 to 80 characters; also the mailbox's display name.
domainIduuidrequired
The domain its mailbox is on.
localPartstringrequired
The part before the @, up to 64 characters.
POST/agents
curl -X POST https://app.rykonmail.com/api/v1/agents \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "API docs example",
    "domainId": "965a5cd1-455b-4c7d-8ae3-f68caf0c7993",
    "localPart": "docs.agent"
  }'
Response
201 Created
{
  "id": "8ca997e9-6ff5-49bc-ba30-a0933f00012a",
  "name": "API docs example",
  "state": "provisioning",
  "address": "docs.agent@northwind-local.test",
  "send_to": "repliers",
  "allow_list": [],
  "daily_limit": 50,
  "approve_all": false,
  "created_at": "2026-10-30T06:16:53.930Z",
  "on_new_mail": "nothing",
  "notify_url": null,
  "notify_secret_set": false,
  "reply_instructions": "",
  "watched_at": null,
  "keys": [],
  "today": {
    "agent_id": "8ca997e9-6ff5-49bc-ba30-a0933f00012a",
    "sent": 0,
    "read": 0,
    "pending": 0
  }
}
GET/agents/approvals

List messages waiting for approval

Every message an agent wrote that waits for a person, across the account, oldest first, with why it was held. Needs mailboxes:read.

Needsmailboxes:readReturns200 OK
GET/agents/approvals
curl https://app.rykonmail.com/api/v1/agents/approvals \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "waiting": [
    {
      "id": "9d60d5d5-5d40-4556-98d5-cf44f59c3a31",
      "agent_id": "8ca997e9-6ff5-49bc-ba30-a0933f00012a",
      "agent": "API docs example",
      "agent_address": "docs.agent@northwind-local.test",
      "recipients": [
        "sam.lee@example.com"
      ],
      "cc": [],
      "subject": "Your order 1",
      "body": "Hi Sam,\n\nYour order is ready to collect from Friday.\n\nAPI docs example",
      "held_because": "every message from this agent waits for a person",
      "error": null,
      "created_at": "2026-10-30T06:17:35.433Z"
    }
  ]
}
POST/agents/outbox/{outboxId}/approve

Approve a held message

Sends a held message, as the agent wrote it or with your edits to the subject and body. Sent once however many times this is called: a message already decided answers 404, and one from a paused agent answers 409 and stays waiting. Answers whether it was sent. Needs mailboxes:write.

Needsmailboxes:writeReturns200 OK

Path parameters

outboxIduuidrequired
The held message's id, from GET /agents/approvals.

Body

subjectstringoptional
A new subject, 1 to 300 characters.
bodystringoptional
A new body, up to 100,000 characters.
POST/agents/outbox/{outboxId}/approve
curl -X POST https://app.rykonmail.com/api/v1/agents/outbox/OUTBOX_ID/approve \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "subject": "Your order is ready",
    "body": "Hi Sam,\n\nYour order is ready to collect from Friday, 9 to 5.\n\nNorthwind Traders"
  }'
Response
200 OK
{
  "state": "sent",
  "messageId": "<f7101765-c6c1-4999-9042-6fb8501a6ed8@northwind-local.test>"
}
POST/agents/outbox/{outboxId}/reject

Reject a held message

Rejects a held message, which is then never sent; the reason is kept in the agent's record. A message already decided answers 404. Needs mailboxes:write.

Needsmailboxes:writeReturns200 OK

Path parameters

outboxIduuidrequired
The held message's id, from GET /agents/approvals.

Body

reasonstringoptional
Why, up to 500 characters.
POST/agents/outbox/{outboxId}/reject
curl -X POST https://app.rykonmail.com/api/v1/agents/outbox/OUTBOX_ID/reject \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "Sent the first one already."
  }'
Response
200 OK
{
  "state": "rejected"
}
GET/agents/{id}

Get an agent

One agent, with its sending rules, its keys (never their secrets) and what it did in the last 24 hours. Needs mailboxes:read.

Needsmailboxes:readReturns200 OK

Path parameters

iduuidrequired
The agent's id.
GET/agents/{id}
curl https://app.rykonmail.com/api/v1/agents/8ca997e9-6ff5-49bc-ba30-a0933f00012a \
  -H "Authorization: Bearer $RYKON_MAIL_KEY"
Response
200 OK
{
  "id": "8ca997e9-6ff5-49bc-ba30-a0933f00012a",
  "name": "API docs example",
  "state": "active",
  "address": "docs.agent@northwind-local.test",
  "send_to": "listed",
  "allow_list": [
    "example.com"
  ],
  "daily_limit": 20,
  "approve_all": true,
  "created_at": "2026-10-30T06:16:53.930Z",
  "on_new_mail": "nothing",
  "notify_url": null,
  "notify_secret_set": false,
  "reply_instructions": "",
  "watched_at": null,
  "keys": [
    {
      "id": "49b887b6-3473-4d17-b39c-1cc56283ea4b",
      "prefix": "eqrf9c76cx0hy",
      "name": "API docs capture",
      "created_at": "2026-10-30T06:17:26.696Z",
      "created_by": "key sa1pteyz44qbm",
      "last_used_at": "2026-10-30T06:17:35.452Z"
    }
  ],
  "today": {
    "agent_id": "8ca997e9-6ff5-49bc-ba30-a0933f00012a",
    "sent": 1,
    "read": 0,
    "pending": 0
  }
}
PUT/agents/{id}

Set an agent's sending rules

Decides what an agent may send without a person: always to colleagues at its own domain, then to anyone, only people who wrote to it first, or an allow list, within a daily limit; with approveAll everything waits for a person. Send every field. Needs mailboxes:write.

Needsmailboxes:writeReturns200 OK

Path parameters

iduuidrequired
The agent's id.

Body

namestringrequired
1 to 80 characters.
sendTo'anyone' | 'repliers' | 'listed'required
Who it may write to without approval.
allowListstring[]required
Up to 200 addresses or domains, for "listed".
dailyLimitintegerrequired
0 to 10,000 messages a day.
approveAllbooleanrequired
Hold every message for a person.
PUT/agents/{id}
curl -X PUT https://app.rykonmail.com/api/v1/agents/8ca997e9-6ff5-49bc-ba30-a0933f00012a \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "API docs example",
    "sendTo": "listed",
    "allowList": [
      "example.com"
    ],
    "dailyLimit": 20,
    "approveAll": true
  }'
Response
200 OK
{
  "id": "8ca997e9-6ff5-49bc-ba30-a0933f00012a",
  "name": "API docs example",
  "state": "active",
  "address": "docs.agent@northwind-local.test",
  "send_to": "listed",
  "allow_list": [
    "example.com"
  ],
  "daily_limit": 20,
  "approve_all": true,
  "created_at": "2026-10-30T06:16:53.930Z",
  "on_new_mail": "nothing",
  "notify_url": null,
  "notify_secret_set": false,
  "reply_instructions": "",
  "watched_at": null,
  "keys": [],
  "today": {
    "agent_id": "8ca997e9-6ff5-49bc-ba30-a0933f00012a",
    "sent": 0,
    "read": 0,
    "pending": 0
  }
}
DELETE/agents/{id}

Remove an agent

Removes an AI agent. Its keys are revoked at once, every message it has waiting for approval is rejected, and its mailbox and every message in it are deleted. Because that cannot be undone, the body must name the agent's address in confirm (400 if it is missing, 409 if it does not match). In the console only the owner or an admin may remove an agent. Needs mailboxes:write.

Needsmailboxes:writeReturns200 OK

Path parameters

iduuidrequired
The agent's id.

Body

confirmstringrequired
The agent's address, typed out.
DELETE/agents/{id}
curl -X DELETE https://app.rykonmail.com/api/v1/agents/8ca997e9-6ff5-49bc-ba30-a0933f00012a \
  -H "Authorization: Bearer $RYKON_MAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "confirm": "helper@example.com"
  }'
Response
200 OK
{
  "removed": true,
  "address": "helper@example.com"
}