Skip to content
CastPDF
Guides

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

Request
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'}}
200
{
  "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.

FieldDescription
tostringRequiredOne email address, without a display name: [email protected], not Ada <[email protected]> and not a list.
subjectstringUp to 200 characters. Default: the PDF's file name, such as invoice-042.pdf.
messagestringPlain 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_tostringOne 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.

JSON
{
  "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.

JSON
"delivery": { "email": { "status": "failed", "reason": "over_limit" } }
statusMeaning
queuedWaiting to be sent, or waiting for a retry.
sentAccepted by our email provider for delivery.
failedNot sent; reason says why (below).
suppressedNot sent, because the address bounced permanently or reported our email as spam before.
reasonMeaning
over_limitThe 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_storedThere was no stored PDF to send.
send_failedEvery 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

LimitValue
PDFs emailed per team in any 24 hoursFree 10, Starter 500, Growth 2,000, Pro 5,000 and Scale 10,000
PDFs emailed to the same address per day20 per team
Across all CastPDF customersAn 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

CodeWhen
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

Updated Something unclear? [email protected], a real person replies.