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 Settings > 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 |
Body Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Sender email address. Must belong to a verified domain. |
from_name | string | Yes | Display name shown alongside the sender address. Max 256 characters. |
to | string[] | Yes | Array of recipient email addresses. |
subject | string | Yes | Email subject line. Max 998 characters. Supports {{variables}}. |
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. Defaults to application/octet-stream. |
Limits: Up to 10 attachments per email, with a combined decoded size of 30 MB. Executable and script MIME types / extensions (e.g. .exe, .bat, .js, .ps1) are rejected.
Template Variables
Use {{variableName}} 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 — matching Amazon SES, which rejects the whole message when a single recipient is invalid. - 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.
Examples
Basic Email
- cURL
- JavaScript
- Python
- C#
- PHP
curl -X POST "https://api.arsel.sa/v1/email/send" \
-H "Authorization: Bearer be_your_api_key" \
-H "Content-Type: application/json" \
-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: {
"Content-Type": "application/json",
Authorization: "Bearer be_your_api_key",
},
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 { id } = await response.json();
console.log(id); // Use this to track delivery
import requests
response = requests.post(
"https://api.arsel.sa/v1/email/send",
headers={
"Content-Type": "application/json",
"Authorization": "Bearer be_your_api_key",
},
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>",
},
)
data = response.json()
print(data["id"])
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer be_your_api_key");
var payload = new
{
from = "noreply@yourdomain.com",
from_name = "My App",
to = new[] { "user@example.com" },
subject = "Welcome to our platform",
html = "<h1>Welcome!</h1><p>Thanks for signing up.</p>"
};
var json = System.Text.Json.JsonSerializer.Serialize(payload);
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
var response = await client.PostAsync("https://api.arsel.sa/v1/email/send", content);
var result = await response.Content.ReadAsStringAsync();
Console.WriteLine(result);
<?php
$ch = curl_init("https://api.arsel.sa/v1/email/send");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Content-Type: application/json",
"Authorization: Bearer be_your_api_key"
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, 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>"
]));
$response = curl_exec($ch);
echo $response;
curl_close($ch);
With Template and Variables
- cURL
- JavaScript
- Python
- C#
- PHP
curl -X POST "https://api.arsel.sa/v1/email/send" \
-H "Authorization: Bearer be_your_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": "order-confirmation-v2",
"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: {
"Content-Type": "application/json",
Authorization: "Bearer be_your_api_key",
},
body: JSON.stringify({
from: "orders@yourdomain.com",
from_name: "Order Notifications",
to: ["customer@example.com"],
subject: "Order {{order_id}} confirmed",
template_id: "order-confirmation-v2",
variables: {
customer_name: "Sara Ahmed",
order_id: "ORD-98765",
total: "149.99 SAR",
},
category: "order-confirmation",
}),
});
const { id } = await response.json();
console.log(id); // Use this to track delivery
import requests
response = requests.post(
"https://api.arsel.sa/v1/email/send",
headers={
"Content-Type": "application/json",
"Authorization": "Bearer be_your_api_key",
},
json={
"from": "orders@yourdomain.com",
"from_name": "Order Notifications",
"to": ["customer@example.com"],
"subject": "Order {{order_id}} confirmed",
"template_id": "order-confirmation-v2",
"variables": {
"customer_name": "Sara Ahmed",
"order_id": "ORD-98765",
"total": "149.99 SAR",
},
"category": "order-confirmation",
},
)
print(response.json())
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer be_your_api_key");
var payload = new
{
from = "orders@yourdomain.com",
from_name = "Order Notifications",
to = new[] { "customer@example.com" },
subject = "Order {{order_id}} confirmed",
template_id = "order-confirmation-v2",
variables = new
{
customer_name = "Sara Ahmed",
order_id = "ORD-98765",
total = "149.99 SAR"
},
category = "order-confirmation"
};
var json = System.Text.Json.JsonSerializer.Serialize(payload);
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
var response = await client.PostAsync("https://api.arsel.sa/v1/email/send", content);
var result = await response.Content.ReadAsStringAsync();
Console.WriteLine(result);
<?php
$ch = curl_init("https://api.arsel.sa/v1/email/send");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Content-Type: application/json",
"Authorization: Bearer be_your_api_key"
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
"from" => "orders@yourdomain.com",
"from_name" => "Order Notifications",
"to" => ["customer@example.com"],
"subject" => "Order {{order_id}} confirmed",
"template_id" => "order-confirmation-v2",
"variables" => [
"customer_name" => "Sara Ahmed",
"order_id" => "ORD-98765",
"total" => "149.99 SAR"
],
"category" => "order-confirmation"
]));
$response = curl_exec($ch);
echo $response;
curl_close($ch);
With Attachments
- cURL
- JavaScript
- Python
- C#
- PHP
curl -X POST "https://api.arsel.sa/v1/email/send" \
-H "Authorization: Bearer be_your_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"
}'
import { readFileSync } from "fs";
const pdfContent = readFileSync("invoice.pdf").toString("base64");
const response = await fetch("https://api.arsel.sa/v1/email/send", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer be_your_api_key",
},
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: pdfContent,
content_type: "application/pdf",
},
],
category: "invoices",
}),
});
const { id } = await response.json();
console.log(id); // Use this to track delivery
import base64
import requests
with open("invoice.pdf", "rb") as f:
pdf_content = base64.b64encode(f.read()).decode()
response = requests.post(
"https://api.arsel.sa/v1/email/send",
headers={
"Content-Type": "application/json",
"Authorization": "Bearer be_your_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": pdf_content,
"content_type": "application/pdf",
}
],
"category": "invoices",
},
)
print(response.json())
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer be_your_api_key");
var pdfBytes = await File.ReadAllBytesAsync("invoice.pdf");
var pdfContent = Convert.ToBase64String(pdfBytes);
var payload = new
{
from = "billing@yourdomain.com",
from_name = "Billing",
to = new[] { "customer@example.com" },
subject = "Your invoice for March 2026",
html = "<p>Please find your invoice attached.</p>",
attachments = new[]
{
new
{
filename = "invoice-march-2026.pdf",
content = pdfContent,
content_type = "application/pdf"
}
},
category = "invoices"
};
var json = System.Text.Json.JsonSerializer.Serialize(payload);
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
var response = await client.PostAsync("https://api.arsel.sa/v1/email/send", content);
Console.WriteLine(await response.Content.ReadAsStringAsync());
<?php
$pdfContent = base64_encode(file_get_contents("invoice.pdf"));
$ch = curl_init("https://api.arsel.sa/v1/email/send");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Content-Type: application/json",
"Authorization: Bearer be_your_api_key"
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, 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" => $pdfContent,
"content_type" => "application/pdf"
]
],
"category" => "invoices"
]));
$response = curl_exec($ch);
echo $response;
curl_close($ch);
Error Responses
- 422 Validation Error
- 400 Bad Request
- 404 Template Not Found
- 401 Unauthorized
- 429 Rate Limited
- 500 Server Error
{
"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, an unverified sender domain, or invalid attachments.
{
"status_code": 400,
"name": "bad_request",
"message": "Too many recipients. Maximum 50 allowed, but 73 provided."
}
{
"status_code": 404,
"name": "not_found",
"message": "Template with id \"...\" not found"
}
{
"status_code": 401,
"name": "unauthorized",
"message": "Invalid or missing API key"
}
{
"status_code": 429,
"name": "rate_limit_exceeded",
"message": "Too many requests. Please retry after the period indicated in the retry-after header."
}
{
"status_code": 500,
"name": "internal_server_error",
"message": "An unexpected error occurred. Please try again later."
}