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

CodeTypeRetryMeaning
202 n/a Accepted and queued. Not yet delivered — delivery outcomes arrive by webhook.
400 invalid_request no A field is missing or malformed, or you sent a header Pharos sets itself. error.field names it.
401 invalid_key no The key is missing, wrong, revoked, or scoped to a different sending domain.
403 domain_unverified no The sending domain has not passed verification. Publish the DNS records and verify it.
404 invalid_request no No endpoint at that path.
405 invalid_request no Wrong method. Allow says which one to use.
422 recipient_suppressed no The recipient is on your suppression list. Nothing was sent, and it does not count against your volume.
429 rate_limited yes Above the rate limit. Retry-After says when to resume.
500 internal_error yes Our fault. The request was well-formed and something on our side did not behave.
502 upstream_error yes The sending service was unreachable, or answered in a way we do not recognise.

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 response403 Forbidden
{
  "error": {
    "type":    "domain_unverified",
    "message": "yourapp.com is not verified",
    "field":   "from",
    "docs":    "/docs/domains"
  }
}
Handling itJavaScript
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}`);
}