Skip to content
On this site

Emails

Send one message or a hundred, look up what happened to it, reschedule it while it is still waiting, and read what was attached.

Before you send

  • The domain in from must be verified on this team. An unverified domain is 403 validation_error, not a silent drop.
  • to, cc and bcc together may not exceed 50 addresses.
  • Send html, text, or both. A message with neither is refused.

Two deviations worth knowing

  • scheduled_at accepts ISO 8601 only, between 1 minute and 30 days from now. Natural language such as "in 1 min" is rejected rather than guessed at.
  • Custom headers cannot override From, To, Cc, Bcc, Subject, Date, Message-ID or Return-Path. Those belong to the envelope we sign; letting a request set them would break DKIM or forge our own identifiers.

Reliability

A 200 from POST /emails means we have durably recorded your message and taken responsibility for sending it. It does not mean the message has reached the mail provider yet, and it certainly does not mean it has been delivered.

Between our call to the mail provider and its answer there is a window where the function can be torn down or the response lost. When that happens we do not know whether the provider holds the message, and we do not send it again: a duplicate email is worse than a visible gap. Instead we look for our own Message-ID in the delivery events that follow. Most such sends resolve within seconds. One that has found no matching event after two hours is marked failed with the reason ambiguous_submission, and both the dashboard and your webhook say so plainly.

So the honest contract is: we will not send your message twice on our own, and if we cannot confirm we sent it at all, we will tell you rather than guess. We do not claim exactly-once delivery, because nobody can.

Finding one again

GET /emails takes five optional filters beside the page parameters — status, api_key_id, start_date, end_date and search — and they are the same ones the dashboard's Sending list uses, so a view you filtered there is a question this endpoint answers identically. search matches the subject, the sender address and the recipient addresses; it does not read the message body.

Counts and rates for the whole account are a different endpoint, on the metrics reference.

Mail sent to you is elsewhere

Everything on this page is mail you sent. Inbound mail is its own resource with its own IDs and its own reads — Receiving — and a received message is never an emails row.

The received figure on GET /emails/metrics is the count of messages the mail provider accepted from you for sending. It is not a count of inbound mail, and it does not move when someone emails you.

Sharing one

POST /emails/{email_id}/share returns a link that opens a read-only view of one sent message, with no sign-in. It is what you paste into a support thread instead of a screenshot.

  • Treat the URL as a secret. Anyone holding it can read the message, and it is the only credential the page asks for.
  • It is returned once. Only a hash of it is stored, so it cannot be retrieved again — create another if you lose it.
  • expires_in is a duration such as 10m or 1 day, defaulting to and capped at 48 hours. An unknown, expired or revoked link all answer the same 404, which says nothing about which of the three it was.

Endpoints

POST /emails

Queue one message for delivery and get its ID back.

Headers

Headers
FieldTypeDescription
Idempotency-Keystring1–256 characters, unique to this send. Replaying it inside 24 hours returns the original response instead of sending again.

Body

Body
FieldTypeDescription
from*stringThe sender, as an address or Name <address>. Its domain must be one this team has verified.
to*string | string[]One recipient or a list. to, cc and bcc together may not exceed 50 addresses.
subject*stringUp to 998 bytes of UTF-8, on one line.
htmlstringThe HTML body. Send html, text or both; at least one is required.
textstringThe plain-text body.
9 more fields (cc, bcc, reply_to, scheduled_at, headers, attachments, tags, template, topic_id)
Body, less common
FieldTypeDescription
ccstring | string[]Visible copies. Counts toward the recipient cap.
bccstring | string[]Blind copies. Counts toward the recipient cap.
reply_tostring | string[]Where replies go. Not counted toward the recipient cap.
scheduled_atstringISO 8601 only, between 1 minute and 30 days from now. Natural language such as "in 1 min" is rejected.
headersobjectCustom message headers, name to value. Headers we own — From, To, Cc, Bcc, Subject, Date, Message-ID, Return-Path, the MIME and DKIM headers, and any List-Unsubscribe* — are refused.
attachmentsobject[]Up to 100 items, each with a filename and exactly one of content (base64) or path (an https URL we fetch). Optional content_type and content_id.
tagsobject[]Up to 50 { name, value } pairs, each matching ^[A-Za-z0-9_-]{1,256}$. Tags come back on every event for this email.
templateobject{ id, variables }. id is a template ID or alias; the template must be published, and its published version supplies html, text and, where the request omits them, subject, from and reply_to. Cannot be combined with html or text. variables maps each declared key to a string or number of at most 2000 characters; a missing key takes its fallback.
topic_idstringReserved for subscription topics in a later phase.
curl -X POST "https://api.rasket.com/emails" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042" \
  -d '{
  "from": "Acme <orders@send.acme.example>",
  "to": ["ronald.williams@example.com"],
  "subject": "Your order has shipped",
  "html": "<p>Order 1042 left the warehouse this morning.</p>"
}'

Response 200

{
  "id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c"
}
  • A 200 means we have accepted and durably recorded the message, not that it has been delivered. Delivery is reported by events.
  • Bodies are capped at 2 MB of html and text combined.
  • Without an Idempotency-Key, a retried request sends a second email.

POST /emails/batch

Up to 100 messages in one request, each independent.

Headers

Headers
FieldTypeDescription
Idempotency-Keystring1–256 characters, unique to this send. Replaying it inside 24 hours returns the original response instead of sending again.

Body

Body
FieldTypeDescription
[]*object[]The body is a JSON array of 100 or fewer send objects, each shaped exactly like POST /emails.
curl -X POST "https://api.rasket.com/emails/batch" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: nightly-digest-2026-09-09" \
  -d '[
  {
    "from": "Acme <orders@send.acme.example>",
    "to": ["ronald.williams@example.com"],
    "subject": "Your order has shipped",
    "html": "<p>Order 1042 left the warehouse this morning.</p>"
  },
  {
    "from": "Acme <orders@send.acme.example>",
    "to": ["ada@example.com"],
    "subject": "Your order has shipped",
    "html": "<p>Order 1043 left the warehouse this morning.</p>"
  }
]'

Response 200

{
  "data": [
    {
      "id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c"
    },
    {
      "id": "9c1d3f28-6b04-4f77-a5e2-1c8d05b3e9a7"
    }
  ]
}
  • One Idempotency-Key covers the whole batch, not each message in it.
  • IDs come back in the order they were sent.

GET /emails

Newest first, cursor paginated.

Query parameters

Query parameters
FieldTypeDescription
limitintegerHow many items to return, 1–100. Defaults to 20.
afterstringReturn the page that follows this item ID. Mutually exclusive with before.
beforestringReturn the page that precedes this item ID. Mutually exclusive with after.
statusstringThe last event the email reached: queued, scheduled, sent, delivery_delayed, delivered, opened, clicked, bounced, complained, failed, suppressed or canceled.
api_key_idstringOnly emails sent with this API key. A revoked key still filters the emails it sent.
start_datestringISO 8601, inclusive. Emails created before this instant are excluded.
end_datestringISO 8601, inclusive. Emails created after this instant are excluded.
searchstringCase-insensitive substring of the subject, the sender address or any recipient address. 1–200 characters.
curl -X GET "https://api.rasket.com/emails?limit=20&search=invoice" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"

Response 200

{
  "object": "list",
  "has_more": true,
  "data": [
    {
      "object": "email",
      "id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
      "from": "Acme <orders@send.acme.example>",
      "to": ["ronald.williams@example.com"],
      "cc": [],
      "bcc": [],
      "reply_to": [],
      "subject": "Your order has shipped",
      "html": "<p>Order 1042 left the warehouse this morning.</p>",
      "text": null,
      "message_id": "<01000199a3c4d5e6-7f8a9b0c@send.acme.example>",
      "created_at": "2026-09-09T10:14:02.118Z",
      "last_event": "delivered"
    }
  ]
}
  • Every filter is optional and additive. Sending none returns the same page it always did.
  • Filters are applied to the query, not to the page, so has_more describes the filtered list and paging through it never skips a row.
  • search looks at the subject, the sender address and the recipient addresses — not the message body. % and _ match literally.

GET /emails/{email_id}

The stored message and the last event it reached.

Path parameters

Path parameters
FieldTypeDescription
email_id*stringThe email's ID.
curl -X GET "https://api.rasket.com/emails/4ef9a417-02e9-4d39-ad75-9611e0fcc33c" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"

Response 200

{
  "object": "email",
  "id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
  "from": "Acme <orders@send.acme.example>",
  "to": ["ronald.williams@example.com"],
  "cc": [],
  "bcc": [],
  "reply_to": [],
  "subject": "Your order has shipped",
  "html": "<p>Order 1042 left the warehouse this morning.</p>",
  "text": null,
  "message_id": "<01000199a3c4d5e6-7f8a9b0c@send.acme.example>",
  "created_at": "2026-09-09T10:14:02.118Z",
  "last_event": "delivered"
}
  • last_event is the furthest state this email has reached, not a history. The full timeline is on the dashboard and on your webhook.

PATCH /emails/{email_id}

Move a scheduled send to a new time.

Path parameters

Path parameters
FieldTypeDescription
email_id*stringThe email's ID.

Body

Body
FieldTypeDescription
scheduled_at*stringISO 8601 only, between 1 minute and 30 days from now.
curl -X PATCH "https://api.rasket.com/emails/4ef9a417-02e9-4d39-ad75-9611e0fcc33c" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "scheduled_at": "2026-09-10T12:00:00.000Z"
}'

Response 200

{
  "object": "email",
  "id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c"
}
  • scheduled_at is the only field this endpoint changes.
  • It works only while last_event is scheduled. Once the message has been dispatched the answer is 409 resource_locked.

POST /emails/{email_id}/cancel

Stop a send that has not gone out yet.

Path parameters

Path parameters
FieldTypeDescription
email_id*stringThe email's ID.
curl -X POST "https://api.rasket.com/emails/4ef9a417-02e9-4d39-ad75-9611e0fcc33c/cancel" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"

Response 200

{
  "object": "email",
  "id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c"
}
  • Cancelling an email that has already been dispatched answers 409 resource_locked; there is no recall.

GET /emails/{email_id}/attachments

What was attached to a sent email, with a signed link for each.

Path parameters

Path parameters
FieldTypeDescription
email_id*stringThe email's ID.

Query parameters

Query parameters
FieldTypeDescription
limitintegerHow many items to return, 1–100. Defaults to 20.
afterstringReturn the page that follows this item ID. Mutually exclusive with before.
beforestringReturn the page that precedes this item ID. Mutually exclusive with after.
curl -X GET "https://api.rasket.com/emails/4ef9a417-02e9-4d39-ad75-9611e0fcc33c/attachments" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"

Response 200

{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "id": "att_5f2c9a1b7e",
      "filename": "invoice-1042.pdf",
      "content_type": "application/pdf",
      "content_disposition": "attachment",
      "size": 48213,
      "download_url": "https://api.rasket.com/emails/4ef9a417-02e9-4d39-ad75-9611e0fcc33c/attachments/att_5f2c9a1b7e/download?expires=1789200000&token=1f0c…",
      "expires_at": "2026-09-09T10:29:02.118Z"
    }
  ]
}
  • Cursors on this list are attachment IDs.

GET /emails/{email_id}/attachments/{attachment_id}

One attachment's metadata and a fresh signed link.

Path parameters

Path parameters
FieldTypeDescription
email_id*stringThe email's ID.
attachment_id*stringThe attachment's ID.
curl -X GET "https://api.rasket.com/emails/4ef9a417-02e9-4d39-ad75-9611e0fcc33c/attachments/att_5f2c9a1b7e" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"

Response 200

{
  "object": "attachment",
  "id": "att_5f2c9a1b7e",
  "filename": "invoice-1042.pdf",
  "content_type": "application/pdf",
  "content_disposition": "attachment",
  "size": 48213,
  "download_url": "https://api.rasket.com/emails/4ef9a417-02e9-4d39-ad75-9611e0fcc33c/attachments/att_5f2c9a1b7e/download?expires=1789200000&token=1f0c…",
  "expires_at": "2026-09-09T10:29:02.118Z"
}

GET /emails/{email_id}/attachments/{attachment_id}/download

Follow the signed link and get the bytes.

Path parameters

Path parameters
FieldTypeDescription
email_id*stringThe email's ID.
attachment_id*stringThe attachment's ID.

Query parameters

Query parameters
FieldTypeDescription
expires*integerPart of the signature. Copy the whole download_url; do not build this yourself.
token*stringThe link's signature, valid for fifteen minutes and for this attachment only.
curl -X GET "https://api.rasket.com/emails/4ef9a417-02e9-4d39-ad75-9611e0fcc33c/attachments/att_5f2c9a1b7e/download?expires=1789200000&token=1f0c9d3b8a72e5461c0d" \
  -H "User-Agent: acme-billing/1.0"
  • This is the only route in the public API that takes no Authorization header: the link carries its own signed authorization so a browser can follow it.
  • A User-Agent is still required, as it is on every other route.
  • The response is the file itself, not JSON.

POST /emails/{email_id}/share

Create a link that opens a read-only view of one sent email, with no sign-in required.

Path parameters

Path parameters
FieldTypeDescription
email_id*stringThe ID of the email.

Body

Body
FieldTypeDescription
expires_instringHow long the link stays valid, as <number><unit> with optional whitespace — 10m, 2 hours, 1 day. Units: s/sec/secs/second/seconds, m/min/mins/minute/minutes, h/hr/hrs/hour/hours, d/day/days. Defaults to 48h and cannot exceed 48 hours.
curl -X POST "https://api.rasket.com/emails/4ef9a417-02e9-4d39-ad75-9611e0fcc33c/share" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "expires_in": "24h"
}'

Response 200

{
  "object": "email",
  "id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
  "url": "https://share.example.com/nQ8vK2xW5yB7dF1hJ4mP6rT9uA3cE0gL2iO",
  "expires_at": "2026-09-11T12:00:00.000Z"
}
  • Treat the URL as a secret: anyone holding it can read the message, and it is the only credential the page asks for.
  • The link is returned once and is not stored — only a hash of it is kept, so it cannot be retrieved again. Create another if you lose it.
  • id is the ID of the email, not of the share, so it is the same value you would pass to GET /emails/{email_id}.
  • expires_at is additive: the reference spec documents only object, id and url.
  • The page is served on a separate origin from the dashboard and renders the message in a sandboxed frame with remote images blocked until the reader loads them.
  • An unknown, expired, or revoked link answers the same 404 page, with nothing said about which of the three it was.

GET /emails/{email_id}/shares

Every link ever created for one email, live and withdrawn alike, newest first.

Path parameters

Path parameters
FieldTypeDescription
email_id*stringThe ID of the email.

Query parameters

Query parameters
FieldTypeDescription
limitintegerHow many items to return, 1–100. Defaults to 20.
afterstringReturn the page that follows this item ID. Mutually exclusive with before.
beforestringReturn the page that precedes this item ID. Mutually exclusive with after.
curl -X GET "https://api.rasket.com/emails/4ef9a417-02e9-4d39-ad75-9611e0fcc33c/shares" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"

Response 200

{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "id": "0198f4c1-0000-7000-8000-000000000000",
      "email_id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
      "status": "active",
      "created_at": "2026-09-09T12:00:00.000Z",
      "expires_at": "2026-09-11T12:00:00.000Z",
      "revoked_at": null,
      "created_by_user_id": null,
      "created_by_api_key_id": "a4d2f0c8-5b31-4e7a-9c62-8f0b1d4e6a75"
    }
  ]
}
  • **There is no url here, and no endpoint can give one back.** Only a hash of each token is stored, so a link that has been lost is revoked and replaced, never re-shown.
  • status is active while the link still opens the page, expired once expires_at has passed, and revoked once it was withdrawn. A link that was withdrawn and has also expired reads revoked.
  • Exactly one of created_by_user_id and created_by_api_key_id is set, recording which credential created the link. Both are null once that member or key is gone; neither restricts who may revoke it.
  • Cursors are share IDs. A cursor naming a share of a different email is 422 invalid_parameter, not an empty page.
  • Revoked links stay in this list, and are deleted only when the email itself is.

DELETE /emails/{email_id}/shares/{share_id}

Stop a link working, without deleting the record that it existed.

Path parameters

Path parameters
FieldTypeDescription
email_id*stringThe ID of the email.
share_id*stringThe ID of the share, as GET /emails/{email_id}/shares reports it.
curl -X DELETE "https://api.rasket.com/emails/4ef9a417-02e9-4d39-ad75-9611e0fcc33c/shares/{share_id}" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"

Response 200

{
  "object": "email_share",
  "id": "0198f4c1-0000-7000-8000-000000000000",
  "revoked_at": "2026-09-10T09:00:00.000Z"
}
  • The link stops working on the next request. There is no cache to wait for.
  • Idempotent: revoking a link that is already revoked answers the instant it *first* stopped working, not the instant of this call.
  • The response says revoked_at rather than the deleted: true other deletes answer with, because the row is kept — a link that was shared and withdrawn is a fact about this email.
  • A share belonging to a different email, or to another team, is 404 — the same answer as one that never existed.
  • Any full_access key or dashboard session may revoke any of this team's links, whichever credential created it: a customer whose key has leaked needs the dashboard to be able to shut the link off.

GET /emails/{email_id}/events

Everything recorded for one email — sent, delivered, bounced, opened — oldest first.

Path parameters

Path parameters
FieldTypeDescription
email_id*stringThe email's ID.
curl -X GET "https://api.rasket.com/emails/4ef9a417-02e9-4d39-ad75-9611e0fcc33c/events" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"

Response 200

{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "email_event",
      "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5f01",
      "type": "email.delivered",
      "recipient": "ronald.williams@example.com",
      "occurred_at": "2026-09-09T09:20:33.412Z",
      "data": {}
    }
  ]
}
  • Ordered by when each event happened, not when it arrived. recipient is null for an event about the whole message.
  • data is the event-specific object in the shape a webhook carries it. To ask what went wrong, see Diagnose an email on the AI reference page.