Skip to main content

Send Email

Queue a transactional email for immediate delivery. Ideal for time-sensitive notifications triggered by user actions:

  • Password resets
  • Welcome messages
  • Order confirmations
  • Two-factor authentication codes

Endpoint​

POST /email/send

Returns: 202 Accepted

Prerequisites​

Verified Domain Required

The sender address in from must belong to a verified domain. You can verify your domain in the Arsel Dashboard under Domains. Domain verification requires adding DNS records (SPF, DKIM, DMARC) to prove ownership. Emails from unverified domains will be rejected.

Headers​

HeaderValueRequired
AuthorizationBearer <your-api-key>Yes
Content-Typeapplication/jsonYes
Idempotency-KeyAny unique string, max 256 charsNo — see Idempotency

Body Parameters​

ParameterTypeRequiredDescription
fromstringYesSender email address. Must belong to a verified domain; see List Domains.
from_namestringYesDisplay name shown alongside the sender address. Max 256 characters.
tostring[]YesArray of recipient email addresses.
subjectstringConditionalEmail subject line. Max 998 characters. Supports {{variables}}. Required with html/text; optional with template_id, where the template's own subject is used if you omit it.
htmlstringConditionalHTML email body. Required if text and template_id are not provided.
textstringConditionalPlain text email body. Required if html and template_id are not provided.
template_idstringConditionalID of a saved template (created via POST /templates or the dashboard). Returns 404 if the template doesn't exist or doesn't belong to your organization.
variablesobjectNoKey-value pairs for {{variable}} replacement in subject, HTML, and text.
reply_tostringNoReply-to email address.
ccstring[]NoCC recipient email addresses.
bccstring[]NoBCC recipient email addresses.
attachmentsobject[]NoFile attachments. See Attachments below.
categorystringNoLabel for grouping emails in analytics. Max 100 characters.
warning

Provide exactly one content source: html, text, or template_id. You may include both html and text together (for multipart emails), but not alongside template_id.

Total Recipients Cap

The combined size of to + cc + bcc must not exceed 50 recipients per message. Requests over this cap are rejected with 400 bad_request (Too many recipients. Maximum 50 allowed, but N provided.). For larger sends, split the request into multiple API calls.

Attachments​

Each attachment object has the following fields:

FieldTypeRequiredDescription
filenamestringYesFilename with extension (e.g., invoice.pdf). Max 255 characters.
contentstringYesBase64-encoded file content.
content_typestringNoMIME type. When omitted or generic (application/octet-stream), the type is taken from the filename extension.
content_idstringNoSends the file inline, so the HTML can show it with <img src="cid:...">. Printable ASCII, no spaces or angle brackets.

Limits: Up to 10 attachments per email. The whole request body may be up to 30 MB, the same ceiling as the SMTP relay; base64 inflates files by a third, so that leaves roughly 22 MB of files. A larger body gets 413 payload_too_large. Executable and script types (e.g. .exe, .bat, .js, .ps1) are not allowed: if any attachment is one of them, the whole request is rejected with 400 attachment_rejected naming the file, and nothing is sent.

Attachments are held encrypted only while the email is being delivered and are deleted once it has been sent. They are never stored with the email record: GET /email/:id returns each file's name, type and size, not its content.

Inline images​

Give the attachment a content_id and reference it from the HTML with cid:. The image is embedded in the message rather than shown as a separate file.

{
"html": "<p>Welcome aboard</p><img src=\"cid:logo\" alt=\"Acme\">",
"attachments": [
{
"filename": "logo.png",
"content": "iVBORw0KGgoAAAANSUhEUgAA...",
"content_type": "image/png",
"content_id": "logo"
}
]
}

Calendar invites​

Attach the .ics file with the invite's method in content_type: text/calendar; method=REQUEST for an invitation or text/calendar; method=CANCEL for a cancellation. Gmail, Outlook and Apple Mail then show the event with accept and decline buttons.

{
"attachments": [
{
"filename": "invite.ics",
"content": "QkVHSU46VkNBTEVOREFSDQpNRVRIT0Q6UkVRVUVTVA0K...",
"content_type": "text/calendar; charset=utf-8; method=REQUEST"
}
]
}

The .ics must carry the same METHOD as the content type and list the recipient as an ATTENDEE. Without a method, the file is delivered as an ordinary attachment.

Template Variables​

Use {{variable_name}} syntax in your subject, HTML, or text content. Pass values in the variables object:

{
"subject": "Your order {{order_id}} has shipped",
"html": "<p>Hi {{name}}, your order is on its way!</p>",
"variables": {
"name": "Sara",
"order_id": "A-12345"
}
}

Variable replacement is case-sensitive. Unmatched placeholders are left as-is.


Response​

{
"id": "01957e3a-4b5c-7d8e-9f0a-1b2c3d4e5f6a"
}
FieldTypeDescription
idstringUnique email ID. Use this to track delivery status via GET /email/:id.
tip

The 202 Accepted response means the email is queued, not yet delivered. Use GET /email/:id to track delivery progress.

Partial Success Semantics​

Recipients go through two validation stages:

  1. Format check (synchronous) — to, cc, bcc, and from must be well-formed email addresses. If any address is malformed, the entire request is rejected with 422 validation_error and nothing is sent.
  2. Deliverability check (asynchronous) — after the request is accepted, each address is checked in the background against MX records, domain reachability, disposable-provider lists, and internal suppression lists.

Because the deliverability check runs after the 202 Accepted response, it cannot change the HTTP status. Addresses that fail it are dropped before send and the remaining valid recipients are still delivered. If every recipient fails this stage, no email is sent — but the request has already returned 202, so there is no error response; the outcome is visible only via GET /email/:id and your analytics.

All addresses you submit appear in GET /email/:id under their respective to / cc / bcc arrays, each with its latest per-recipient status. A recipient dropped at the deliverability check never receives tracking events, so it remains in sent status — an address stuck at sent (never advancing to delivered/bounced) indicates it was likely filtered before send.


Idempotency​

Sending the same email twice — for example because a network timeout made you retry — could send the email twice. To make a retry safe, send an Idempotency-Key header whose value is unique to that email (for example welcome-user/123):

  • The first request with a given key is processed normally and its response is stored.
  • Any later request with the same key (within 24 hours) is not reprocessed — it returns the original response unchanged, plus an Idempotent-Replayed: true response header. The side effect happens only once.
  • After 24 hours the key expires and may be reused.

The key can be any unique string up to 256 characters. A UUID works, but a value derived from the entity the request is about — like welcome-user/123 — is easier to regenerate identically on a retry. Keys are scoped per endpoint, so the same value can be reused independently across the send-email, send-SMS, and send-event endpoints without colliding.

Reusing a key with a different payload

Reuse a key with a different request body and the request is rejected with 409 invalid_idempotent_request, which protects you from receiving the wrong cached response. Use a fresh key for a genuinely different request.

Idempotency error responses

StatusnameWhen
400invalid_idempotency_keyThe key is empty or longer than 256 characters.
409invalid_idempotent_requestThe key was already used with a different request body.
503concurrent_idempotent_requestsAn earlier request with the same key is still being processed — retry shortly.

Examples​

Basic Email​

curl -X POST "https://api.arsel.sa/v1/email/send" \
-H "Authorization: Bearer $ARSEL_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: welcome-user/123" \
-d '{
"from": "noreply@yourdomain.com",
"from_name": "My App",
"to": ["user@example.com"],
"subject": "Welcome to our platform",
"html": "<h1>Welcome!</h1><p>Thanks for signing up.</p>"
}'

With Template and Variables​

curl -X POST "https://api.arsel.sa/v1/email/send" \
-H "Authorization: Bearer $ARSEL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "orders@yourdomain.com",
"from_name": "Order Notifications",
"to": ["customer@example.com"],
"subject": "Order {{order_id}} confirmed",
"template_id": "0192a1b2-c3d4-7e5f-9000-abcdef123456",
"variables": {
"customer_name": "Sara Ahmed",
"order_id": "ORD-98765",
"total": "149.99 SAR"
},
"category": "order-confirmation"
}'

With Attachments​

curl -X POST "https://api.arsel.sa/v1/email/send" \
-H "Authorization: Bearer $ARSEL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "billing@yourdomain.com",
"from_name": "Billing",
"to": ["customer@example.com"],
"subject": "Your invoice for March 2026",
"html": "<p>Please find your invoice attached.</p>",
"attachments": [
{
"filename": "invoice-march-2026.pdf",
"content": "JVBERi0xLjQK...",
"content_type": "application/pdf"
}
],
"category": "invoices"
}'

Error Responses​

{
"status_code": 422,
"name": "validation_error",
"message": "to must be an array, each value in to must be an email"
}