Email delivery
Email each generated PDF to your customer with deliver.email, from direct, background and batch requests. Who can be emailed, limits and statuses.
On this page
Add deliver.email to a request and CastPDF emails the finished PDF to the address you give, so you do not need your own email service to send an invoice or a certificate. It works with direct requests to POST /v1/pdf, with background PDFs ("async": true) and with every item of a batch.
Email delivery is built for sending a document to the person it belongs to. It is not a newsletter tool: there are limits per plan and per recipient, and new or Free workspaces can email only their own team.
Send a PDF by email
curl https://api.castpdf.com/v1/pdf \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"template_id": "8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b",
"data": {"number": "INV-042", "customer": {"name": "Acme Ltd"}, "total": 1234.5},
"filename": "invoice-042",
"response": "url",
"deliver": {
"email": {
"to": "[email protected]",
"subject": "Your invoice INV-042",
"message": "Hello,\n\nplease find your invoice attached.\n\nThank you!",
"reply_to": "[email protected]"
}
}
}'const res = await fetch('https://api.castpdf.com/v1/pdf', {
method: 'POST',
headers: { Authorization: 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' },
body: JSON.stringify({
template_id: '8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b',
data: { number: 'INV-042', customer: { name: 'Acme Ltd' }, total: 1234.5 },
filename: 'invoice-042',
response: 'url',
deliver: {
email: {
to: '[email protected]',
subject: 'Your invoice INV-042',
message: 'Hello,\n\nplease find your invoice attached.\n\nThank you!',
reply_to: '[email protected]',
},
},
}),
});
const doc = await res.json();
if (!res.ok) throw new Error(`${doc.error.code}: ${doc.error.message}`);
console.log(doc.delivery); // { email: { status: 'queued' } }import requests
res = requests.post(
"https://api.castpdf.com/v1/pdf",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"template_id": "8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b",
"data": {"number": "INV-042", "customer": {"name": "Acme Ltd"}, "total": 1234.5},
"filename": "invoice-042",
"response": "url",
"deliver": {
"email": {
"to": "[email protected]",
"subject": "Your invoice INV-042",
"message": "Hello,\n\nplease find your invoice attached.\n\nThank you!",
"reply_to": "[email protected]",
}
},
},
timeout=60,
)
doc = res.json()
if not res.ok:
raise RuntimeError(f"{doc['error']['code']}: {doc['error']['message']}")
print(doc["delivery"]) # {'email': {'status': 'queued'}}{
"id": "3f0c9a4e-6b1d-4c2a-9a57-0d8e4b7f21c3",
"status": "succeeded",
"pages": 1,
"bytes": 48213,
"mode": "print",
"test": false,
"template_id": "8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b",
"template_version": 4,
"error_code": null,
"created_at": "2026-10-03T10:15:02.123Z",
"expires_at": "2026-10-10T10:15:02.123Z",
"url": "https://api.castpdf.com/v1/files/3f0c9a4e-6b1d-4c2a-9a57-0d8e4b7f21c3?exp=1791972902&sig=Jx0k…",
"thumbnail_url": "https://api.castpdf.com/v1/thumbnails/documents/3f0c9a4e-6b1d-4c2a-9a57-0d8e4b7f21c3.png?exp=1791375302&sig=Tb8q…",
"filename": "invoice-042.pdf",
"metadata": null,
"delivery": { "email": { "status": "queued" } },
"blocked_resources": [],
"warnings": []
}The PDF is made first, then the email is queued and sent shortly after. The request does not wait for the email: delivery shows queued and changes to sent once the email has gone out. With the default "response": "binary" you get the PDF itself as usual and can read the delivery state later with GET /v1/pdf/:id. An emailed PDF is always stored, even with a test key, because the email needs it.
| Field | Description |
|---|---|
tostringRequired | One email address, without a display name: [email protected], not Ada <[email protected]> and not a list. |
subjectstring | Up to 200 characters. Default: the PDF's file name, such as invoice-042.pdf. |
messagestring | Plain text, up to 5,000 characters; line breaks are kept. Default: "Your document invoice-042.pdf is attached." (or "is ready." when it is sent as a link). |
reply_tostring | One email address that receives replies, such as your billing address. Default: none. |
Background PDFs and batches
Background PDFs: add deliver to a request with "async": true. The email is sent as soon as the PDF is ready. The checks below (address, who can be emailed, limits) run when you send the request, so a refused email is an error right away, not a failed job later.
Batches: each item can have its own deliver.email, and the batch's deliver.email holds defaults that every item uses (an item's own fields win). Only items that end up with a to are emailed. For a spreadsheet batch, email_column names the column with each row's address. All of a batch's recipients are checked against the limits together when you create the batch.
{
"template_id": "8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b",
"deliver": { "email": { "subject": "Your certificate", "reply_to": "[email protected]" } },
"items": [
{ "data": { "name": "Ada Lovelace" }, "deliver": { "email": { "to": "[email protected]" } } },
{ "data": { "name": "Grace Hopper" } }
]
}Here Ada gets her certificate by email; Grace's is only in the batch's ZIP.
Who can be emailed
Live mode, paid plans: anyone, once a team owner or admin has verified their email address. Until then, every request with deliver.email is refused with email_not_verified (403).
Members only: in these cases PDFs can be emailed only to verified email addresses of your own team's members:
- on the Free plan;
- during a workspace's first 7 days;
- while a payment is overdue or the subscription is paused;
- in test mode (a test key,
"test": true, or a test batch).
Any other address is refused with invalid_request (400) and details: { "field": "deliver.email.to", "reason": "members_only" }. This keeps CastPDF's email from being used to send unwanted mail from new accounts. In test mode it also means you can try the whole flow safely: the email goes to you, with a watermarked PDF.
What the email looks like
- Sender: CastPDF's sending address, with the sender name "CastPDF". With
reply_to, replies go to your address. - Subject and text: yours, or the defaults above.
- Footer: "Sent by" and your team's name, "with CastPDF.", and a line for reporting abuse to [email protected]. A test document also says "This is a test document."
- The PDF: attached when it is up to 10 MB. A larger PDF is sent as a download link instead, with the date until which it works: the PDF's own expiry (your plan's retention period).
Delivery status
Every document that was emailed has a delivery field (on GET /v1/pdf/:id, in the document log and in the "response": "url" answer). Documents without an email have "delivery": null.
"delivery": { "email": { "status": "failed", "reason": "over_limit" } }status | Meaning |
|---|---|
queued | Waiting to be sent, or waiting for a retry. |
sent | Accepted by our email provider for delivery. |
failed | Not sent; reason says why (below). |
suppressed | Not sent, because the address bounced permanently or reported our email as spam before. |
reason | Meaning |
|---|---|
over_limit | The email passed the checks when you sent the request, but met a limit when it was queued (for example many items of one batch at the same moment). It is not sent later. |
not_stored | There was no stored PDF to send. |
send_failed | Every attempt to send it failed. |
sent means the email left CastPDF; it does not prove that it reached the inbox. The pdf.created webhook does not carry delivery: read it from GET /v1/pdf/:id when you need it.
Retries
A send that fails, for example because the email provider is briefly unavailable, is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours. While it waits, delivery stays queued; after the last attempt it becomes failed with send_failed. An email is never sent twice for the same document, even when you repeat a request with the same Idempotency-Key: the repeat returns the first document and queues nothing new.
Limits
| Limit | Value |
|---|---|
| PDFs emailed per team in any 24 hours | Free 10, Starter 500, Growth 2,000, Pro 5,000 and Scale 10,000 |
| PDFs emailed to the same address per day | 20 per team |
| Across all CastPDF customers | An hourly ceiling, so one account cannot affect everyone's email |
Test-mode emails count toward these limits too. The limits are checked when you send the request: over one, the request fails with email_limit_reached (429) and nothing is rendered or counted. details.scope says which limit: team_daily (with limit and used), recipient_daily or service_hourly. They are checked again, exactly, when each email is queued; an email that does not fit then is not sent and shows failed with over_limit.
Suppressed addresses
When an email to an address bounces permanently, or its owner marks our email as spam, the address is put on a suppression list. Later PDFs to it are not sent and show suppressed, while the PDF itself is still made and stored. If a customer says they get nothing, check the delivery status, then ask for another address.
Errors
| Code | When |
|---|---|
invalid_request (400) | to is missing or not one valid address, reply_to is not one valid address, or the recipient is not allowed (details.reason: "members_only", see above) |
email_not_verified (403) | Live mode, and no team owner or admin has verified their email address yet |
email_limit_reached (429) | Over a daily or hourly limit; details.scope says which |
All codes are on the errors page.
XRechnung cannot be emailed yet
deliver cannot be combined with an XRechnung e-invoice ("standard": "xrechnung-cii" or "xrechnung-ubl"): the request is refused with invalid_request. Ask for "response": "url", download the XML and send it through your own channel. Factur-X and ZUGFeRD PDFs can be emailed like any other PDF. See e-invoicing.
Next steps
- Batches: email hundreds of PDFs from one request.
- PDFs from a spreadsheet: one emailed PDF per row.
- Webhooks and signatures: know when each PDF is ready.