Send an email
/email/v1/emailsSends an email from one of your templates to one address, with your values filled in. The email is charged and queued at once, then leaves within seconds.
Your key needs the Email API turned on, and in live your team must be approved. Your domain, senders, brand and templates are set up in the app. See Email.
Request body
Content-Type: application/json
- The one address to send to. Up to 254 characters.Example:
ada@example.com - The handle of one of your templates. Each template's handle is under Email › Templates.Example:
verification-code - A value for each of the template's
{{variables}}, by name. Every variable the template uses needs one. Each value is text, up to 1,000 characters. Leave it out for a template without variables.Example:{"first_name": "Ada", "code": "482913", "expires_in": "10 minutes"} - The address of one of your senders. Its name goes with it. Leave it out to send from your default sender.Example:
billing@mail.example.com - Your own reference for the email, such as a sign-up or order number. Up to 64 characters, unique within your team. Reusing one returns the first email without charging again.Example:
signup-8812
If a request times out, send it again with the same externalId. If the first request went through, you get its email back with 200. You're charged once. See Retries and externalId.
Example request
- curl
- Node.js
- Python
- PHP
curl -X POST "https://sandbox-api.ponitor.com/email/v1/emails" \
-H "apiKey: $PONITOR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "ada@example.com",
"template": "verification-code",
"values": {
"first_name": "Ada",
"code": "482913",
"expires_in": "10 minutes"
},
"externalId": "signup-8812"
}'
const response = await fetch('https://sandbox-api.ponitor.com/email/v1/emails', {
method: 'POST',
headers: {
apiKey: process.env.PONITOR_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
to: 'ada@example.com',
template: 'verification-code',
values: {
first_name: 'Ada',
code: '482913',
expires_in: '10 minutes',
},
externalId: 'signup-8812',
}),
});
console.log(response.status, await response.json());
import os
import requests
response = requests.post(
"https://sandbox-api.ponitor.com/email/v1/emails",
headers={"apiKey": os.environ["PONITOR_API_KEY"]},
json={
"to": "ada@example.com",
"template": "verification-code",
"values": {
"first_name": "Ada",
"code": "482913",
"expires_in": "10 minutes",
},
"externalId": "signup-8812",
},
timeout=30,
)
print(response.status_code, response.json())
<?php
$ch = curl_init('https://sandbox-api.ponitor.com/email/v1/emails');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'apiKey: ' . getenv('PONITOR_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'to' => 'ada@example.com',
'template' => 'verification-code',
'values' => [
'first_name' => 'Ada',
'code' => '482913',
'expires_in' => '10 minutes',
],
'externalId' => 'signup-8812',
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
echo $status, ' ', $body, PHP_EOL;
To send from another of your senders, add "from": "billing@mail.example.com".
Responses
- 201
- 200
- 400
- 401
- 403
- 404
- 429
- 500
- 503
Created. The email was charged and queued. The body is the email, with status QUEUED. Follow it with Get an email or the email.* webhook events.
{
"id": "3f0e8a52-6f4b-4d0e-9b8a-2c1d0e9f8a7b",
"externalId": "signup-8812",
"from": "\"Example\" <hello@mail.example.com>",
"to": "ada@example.com",
"subject": "Your code is 482913",
"templateName": "Verification Code",
"status": "QUEUED",
"cost": 0.002,
"refunded": false,
"failureReason": null,
"createdAt": "2026-11-02T12:00:00.000Z",
"sentAt": null
}
OK. The externalId was already used. The body is the first email, with its status now. No new email was sent and nothing more was charged, even if to, template or values differ.
Bad Request. The request was refused. Nothing was charged and no email was made. A field is invalid (E01001), the template needs a value you didn't send, you have no sender or the sender's domain isn't verified (E01002), or the balance is too low (E03001). See Refused emails for each message.
{
"error": {
"type": "validation_error",
"code": "E01001",
"name": "ValidationError",
"message": "The request has 1 problem.",
"doc_url": "https://docs.ponitor.com/api/errors#E01001",
"request_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
"param": "externalId",
"details": [
{ "param": "externalId", "message": "externalId is required" }
]
}
}
Every error has this error object. See The error body.
Unauthorized. The API key is missing (E02001), wrong or revoked (E02002), or sent to the other base URL (E02003). See API keys and base URLs.
Forbidden. Your team hasn't turned on the Email API, in live isn't approved yet (E02010), or its email sending is paused (E02017). Nothing was charged. See When sending is paused.
{
"error": {
"type": "permission_error",
"code": "E02017",
"name": "EmailSendingPaused",
"message": "Your team's email sending is paused, so nothing was sent.",
"doc_url": "https://docs.ponitor.com/api/errors#E02017",
"request_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f"
}
}
Not Found. No template has that handle, or from isn't one of your senders (E01005). Nothing was charged.
{
"error": {
"type": "not_found_error",
"code": "E01005",
"name": "NotFound",
"message": "No template has the handle verification-code. Each template's handle is under Email › Templates.",
"doc_url": "https://docs.ponitor.com/api/errors#E01005",
"request_id": "d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f7a"
}
}
Too Many Requests. Your team went over its rate limit (E01011). Nothing was charged. Wait, then send the same request again with the same externalId.
Server Error. Ponitor couldn't take the email (E01000). Retry with the same externalId: if the first try was charged, you get its email back, charged once.
Service Unavailable. Email isn't available for a moment (E01012). Nothing was sent or charged. Try again later with the same externalId.
The email object
| Field | Description |
|---|---|
id | Ponitor's ID for the email. Use it with GET /emails/{id}. |
externalId | The reference you sent. null for an email sent from the app. |
from | The sender's name and address, as inboxes show it. |
to | The address it was sent to. |
subject | The subject, with your values filled in. |
templateName | The name of the template it was drawn from, as it was when sent. |
status | QUEUED, SENT, DELIVERED, DELAYED, BOUNCED, COMPLAINED or FAILED. See What happens to an email. |
cost | What your account was charged, in USD. |
refunded | Whether its cost went back to your account. true for a FAILED email, and only then. |
failureReason | Why a FAILED email failed. null otherwise. |
createdAt | When it was charged and queued. |
sentAt | When it left for the inbox. null until then. |
Get an email also returns events, everything that happened to it.
Refused emails
A refused email answers 400 or 404, and nothing is charged. Fix the request and send it again.
| Problem | Code | Message |
|---|---|---|
A field is missing or invalid, such as to | E01001 | Each field's message is in details, for example to is required |
| A variable the template uses has no value | E01002 | Give a value for {{code}}. |
No from, and your team has no default sender | E01002 | Add a sender under Email › Domains first. |
| The sender's domain isn't verified | E01002 | This sender's domain isn't verified. Check it under Email › Domains. |
| The request names a workspace | E01002 | Email is your team's, not a workspace's. Leave out workspace. |
| No template has the handle | E01005 | No template has the handle verification-code. Each template's handle is under Email › Templates. |
from isn't one of your senders | E01005 | billing@mail.example.com isn't one of your email senders. Add it under Email › Domains. |
| Your account can't cover the email | E03001 | Insufficient account balance. Top up under Billing to continue. |