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
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
| Header | Value | Required |
|---|---|---|
Authorization | Bearer <your-api-key> | Yes |
Content-Type | application/json | Yes |
Idempotency-Key | Any unique string, max 256 chars | No — see Idempotency |
Body Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Sender email address. Must belong to a verified domain; see List Domains. |
from_name | string | Yes | Display name shown alongside the sender address. Max 256 characters. |
to | string[] | Yes | Array of recipient email addresses. |
subject | string | Conditional | Email 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. |
html | string | Conditional | HTML email body. Required if text and template_id are not provided. |
text | string | Conditional | Plain text email body. Required if html and template_id are not provided. |
template_id | string | Conditional | ID 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. |
variables | object | No | Key-value pairs for {{variable}} replacement in subject, HTML, and text. |
reply_to | string | No | Reply-to email address. |
cc | string[] | No | CC recipient email addresses. |
bcc | string[] | No | BCC recipient email addresses. |
attachments | object[] | No | File attachments. See Attachments below. |
category | string | No | Label for grouping emails in analytics. Max 100 characters. |
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.
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:
| Field | Type | Required | Description |
|---|---|---|---|
filename | string | Yes | Filename with extension (e.g., invoice.pdf). Max 255 characters. |
content | string | Yes | Base64-encoded file content. |
content_type | string | No | MIME type. When omitted or generic (application/octet-stream), the type is taken from the filename extension. |
content_id | string | No | Sends 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"
}
| Field | Type | Description |
|---|---|---|
id | string | Unique email ID. Use this to track delivery status via GET /email/:id. |
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:
- Format check (synchronous) —
to,cc,bcc, andfrommust be well-formed email addresses. If any address is malformed, the entire request is rejected with422 validation_errorand nothing is sent. - 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: trueresponse 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.
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
| Status | name | When |
|---|---|---|
400 | invalid_idempotency_key | The key is empty or longer than 256 characters. |
409 | invalid_idempotent_request | The key was already used with a different request body. |
503 | concurrent_idempotent_requests | An earlier request with the same key is still being processed — retry shortly. |
Examples
Basic Email
- cURL
- JavaScript
- Python
- C#
- PHP
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>"
}'
const response = await fetch("https://api.arsel.sa/v1/email/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ARSEL_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "welcome-user/123",
},
body: JSON.stringify({
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>",
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(`${data.name}: ${data.message}`);
}
console.log(data);
import os
import requests
response = requests.post(
"https://api.arsel.sa/v1/email/send",
headers={
"Authorization": f"Bearer {os.environ['ARSEL_API_KEY']}",
"Idempotency-Key": "welcome-user/123",
},
json={
"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>",
},
)
response.raise_for_status()
print(response.json())
using System.Net.Http.Headers;
using System.Text;
var client = new HttpClient(); // share one instance across requests
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("ARSEL_API_KEY"));
var request = new HttpRequestMessage(HttpMethod.Post, "https://api.arsel.sa/v1/email/send")
{
Content = new StringContent(
"""
{
"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>"
}
""",
Encoding.UTF8,
"application/json"),
};
request.Headers.Add("Idempotency-Key", "welcome-user/123");
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
throw new HttpRequestException($"{(int)response.StatusCode}: {body}");
Console.WriteLine(body);
<?php
$ch = curl_init("https://api.arsel.sa/v1/email/send");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("ARSEL_API_KEY"),
"Content-Type: application/json",
"Idempotency-Key: welcome-user/123",
],
CURLOPT_POSTFIELDS => json_encode([
"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>",
]),
]);
$data = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) >= 400) {
throw new Exception("{$data['name']}: {$data['message']}");
}
print_r($data);
With Template and Variables
- cURL
- JavaScript
- Python
- C#
- PHP
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"
}'
const response = await fetch("https://api.arsel.sa/v1/email/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ARSEL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
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",
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(`${data.name}: ${data.message}`);
}
console.log(data);
import os
import requests
response = requests.post(
"https://api.arsel.sa/v1/email/send",
headers={"Authorization": f"Bearer {os.environ['ARSEL_API_KEY']}"},
json={
"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",
},
)
response.raise_for_status()
print(response.json())
using System.Net.Http.Headers;
using System.Text;
var client = new HttpClient(); // share one instance across requests
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("ARSEL_API_KEY"));
var request = new HttpRequestMessage(HttpMethod.Post, "https://api.arsel.sa/v1/email/send")
{
Content = new StringContent(
"""
{
"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"
}
""",
Encoding.UTF8,
"application/json"),
};
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
throw new HttpRequestException($"{(int)response.StatusCode}: {body}");
Console.WriteLine(body);
<?php
$ch = curl_init("https://api.arsel.sa/v1/email/send");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("ARSEL_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"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",
]),
]);
$data = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) >= 400) {
throw new Exception("{$data['name']}: {$data['message']}");
}
print_r($data);
With Attachments
- cURL
- JavaScript
- Python
- C#
- PHP
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"
}'
const response = await fetch("https://api.arsel.sa/v1/email/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ARSEL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
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",
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(`${data.name}: ${data.message}`);
}
console.log(data);
import os
import requests
response = requests.post(
"https://api.arsel.sa/v1/email/send",
headers={"Authorization": f"Bearer {os.environ['ARSEL_API_KEY']}"},
json={
"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",
},
)
response.raise_for_status()
print(response.json())
using System.Net.Http.Headers;
using System.Text;
var client = new HttpClient(); // share one instance across requests
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("ARSEL_API_KEY"));
var request = new HttpRequestMessage(HttpMethod.Post, "https://api.arsel.sa/v1/email/send")
{
Content = new StringContent(
"""
{
"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"
}
""",
Encoding.UTF8,
"application/json"),
};
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
throw new HttpRequestException($"{(int)response.StatusCode}: {body}");
Console.WriteLine(body);
<?php
$ch = curl_init("https://api.arsel.sa/v1/email/send");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("ARSEL_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"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",
]),
]);
$data = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) >= 400) {
throw new Exception("{$data['name']}: {$data['message']}");
}
print_r($data);
Error Responses
- 422 Validation Error
- 400 Bad Request
- 403 Suspended
- 500 Server Error
- 404 Not Found
- 401 Unauthorized
- 429 Rate Limited
{
"status_code": 422,
"name": "validation_error",
"message": "to must be an array, each value in to must be an email"
}
Business validation failures — for example too many total recipients or an unverified sender domain. A disallowed attachment also returns 400, with name: attachment_rejected.
{
"status_code": 400,
"name": "bad_request",
"message": "Too many recipients. Maximum 50 allowed, but 73 provided."
}
Email sending is suspended for the organization — see Sender reputation.
{
"status_code": 403,
"name": "sending_suspended",
"message": "Email sending is suspended for this organization. Reason: high bounce/complaint rate. Review on the dashboard or contact support to unsuspend."
}
{
"status_code": 500,
"name": "internal_server_error",
"message": "An unexpected error occurred. Please try again later."
}
{
"status_code": 404,
"name": "not_found",
"message": "Template with id \"...\" not found"
}
{
"status_code": 401,
"name": "unauthorized",
"message": "Invalid API Key"
}
{
"status_code": 429,
"name": "rate_limit_exceeded",
"message": "Too many requests. Please retry after the period indicated in the retry-after header."
}