API reference / Errors and limits
Errors and limits
Failures come back as HTTP status codes, never hidden inside a 200. If you get a
2xx, the message was accepted.
The error shape
Every error has the same four fields. type is the stable, machine-readable part —
branch on it. message is written for a human reading logs and may be reworded, so do
not parse it. field appears when one field is at fault. docs points at
the page that explains it.
Status codes
error.field names it. Allow says which one to use. Retry-After says when to resume. What to retry
The rule is simple enough to write once and never think about again: a
4xx will fail again — the request is wrong, and sending it a second time
changes nothing except your rate-limit budget. 429 and 5xx are worth
retrying with backoff.
Retrying is only safe if a retry cannot become a duplicate, which is what
Idempotency-Key is for. Send one on anything you might repeat.
Rate limit
100 requests per minute. One request can carry several recipients, so this is a ceiling on calls rather than on mail — 144,000 requests a day, roughly nine times the daily average of the largest plan.
Go over it and you get 429 with a Retry-After header in seconds. Pharos
does not queue on your behalf: a rejected request was not sent, and it is yours to retry.
{
"error": {
"type": "domain_unverified",
"message": "yourapp.com is not verified",
"field": "from",
"docs": "/docs/domains"
}
} const res = await fetch("https://api.pharos.email/v1/send", {
method: "POST",
headers: {
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
// a retry can never become a duplicate
"Idempotency-Key": orderId
},
body: JSON.stringify(message)
});
if (res.status === 429 || res.status >= 500) {
// worth retrying
const wait = Number(res.headers.get("Retry-After") ?? 2);
return retryAfter(wait);
}
if (!res.ok) {
// will fail again. fix the request.
const { error } = await res.json();
throw new Error(`${error.type}: ${error.message}`);
}