Short reference for integrating this package. Prefer this page + the DocRaptor
moduledoc over guessing from HTTP examples on docraptor.com.
Entrypoint
{:ok, client} = DocRaptor.Client.new(api_key: key)—:testdefaults totrue- Pick a create function (table below)
Match on
{:ok, result} | {:error, %DocRaptor.Error{}}— no bang APIs, and%DocRaptor.Error{}is not an exception (nodefexception; do notraiseit)
The client is a plain struct (not a GenServer). Building per call or storing in app config/state are both fine.
Which create function?
| Need | Call | Success type | Notes |
|---|---|---|---|
| PDF/XLS bytes now | DocRaptor.create_doc/2 | %DocRaptor.Document{body: binary()} | Sync ~60s (receive_timeout); retry: false |
| Long / background job | create_async_doc/2 then await_async_doc/3 or poll / webhook | %AsyncJob{} → %Document{} | await blocks with Process.sleep — use a worker, not a LiveView/controller process |
| Public download URL (paid add-on) | create_hosted_doc/2 | %Status{download_url, download_id} | Not raw bytes |
| Hosted + background | create_hosted_async_doc/2 | %AsyncJob{} | Prefer status/download_url; await_async_doc hits /download and may not match hosted billing intent |
Custom HTTP: build %Req.Request{}, then DocRaptor.Client.attach/2, then Req.request/1.
Document attrs (doc_attrs)
Accepted as a map or keyword list. Atom or string keys both work.
Nested prince_options keys are stringified. This client validates only:
| Field | Required? | Notes |
|---|---|---|
name | yes (client) | Non-empty string. DocRaptor treats name as optional; we require it. |
document_type or type | yes (client) | Must be present; value is to_string’d and sent as-is. DocRaptor expects pdf / xls / xlsx. If both keys are set, document_type is used. |
document_content xor document_url | yes | Exactly one; both → :invalid_document |
test | no | Overrides client default (true) |
For Excel, send spreadsheet XML in document_content (or a URL to XML) with
document_type: :xls or :xlsx. PDF content is HTML.
Everything else is passed through to DocRaptor JSON, including:
javascript,ignore_console_messages,pipeline,referrer,tag,strict,helpcallback_url(async webhook)hosted_download_limit,hosted_expires_at(hosted)prince_optionsmap — common keys:media("print"\|"screen"),baseurl,encrypt,disallow_print,javascript,max_passes, passwords, etc.
Full upstream list: https://docraptor.com/documentation/api/parameters
JavaScript engines (don't confuse them)
| Flag | Engine | Use when |
|---|---|---|
javascript: true | DocRaptor’s engine | Charts, Typekit, most page JS (default choice) |
prince_options: %{javascript: true} | Prince’s engine | Canvas / Prince PDF JS APIs |
Enabling both runs JS twice — usually avoid. pipeline selects Prince/JS
runtime versions; ignore_console_messages: true keeps console.log from
failing the job (defaults vary by pipeline on DocRaptor’s side).
Async IDs
create_async_* → %AsyncJob{status_id}
│
▼
get_async_doc_status(client, status_id)
│
├─ :queued | :working → poll again
├─ :failed → error (validation_errors || message)
└─ :completed → download_id (and maybe download_url if hosted)
│
▼
get_async_doc(client, download_id) # non-hosted bytesawait_async_doc/3 does status polling + get_async_doc/2 for you.
Options: :interval_ms (default 1000), :timeout_ms (default 600_000).
callback_url webhook
On successful async completion only, DocRaptor POSTs your callback_url
(form parameters). On generation failure the callback is not called — poll
/status instead.
| Param | Meaning |
|---|---|
download_url | URL to fetch the finished document |
download_id | Per DocRaptor: corresponds to the create response status_id |
Retries: if your endpoint does not return 2xx within ~10s, DocRaptor retries the callback up to three times.
# Phoenix controller sketch
def docraptor_callback(conn, %{"download_url" => url, "download_id" => id}) do
# enqueue worker to fetch url / record id — respond 2xx quickly
json(conn, %{ok: true})
endErrors
%DocRaptor.Error{reason: reason, message: msg, status: http_or_nil, body: raw}reason | When | status | body |
|---|---|---|---|
:missing_api_key | Client.new/1 | nil | nil |
:invalid_document | Local encode validation | nil | often nil |
:api | Non-2xx or bad JSON shape | HTTP code | usually a binary (often XML with <error> tags); sometimes a map if already decoded |
:transport | Network / Req failure | nil | underlying reason |
:timeout | await_async_doc/3 deadline | nil | last %Status{} |
:async_failed | Async status failed | nil | %Status{}; message prefers validation_errors then message |
For :api, :message is derived from the body: <error>...</error> segments
joined with newlines when present, otherwise the raw body string (or
inspect/1 for maps).
Common HTTP statuses you may see on :api (upstream; not exhaustive): 401
auth, 422 validation, 429 rate limit, 5xx server errors.
Retries
Client.attach/2 sets retry: false. This package does not auto-retry.
If you retry at the app layer:
- Worth retrying (with backoff): 429, transient 5xx,
:transport - Usually do not retry: 401, 422,
:invalid_document,:async_failed
case DocRaptor.create_doc(client, attrs) do
{:ok, %DocRaptor.Document{body: pdf}} ->
{:ok, pdf}
{:error, %DocRaptor.Error{reason: :invalid_document, message: msg}} ->
{:error, msg}
{:error, %DocRaptor.Error{reason: :api, status: status, message: msg}}
when status in [429, 500, 502, 503, 504] ->
{:retry, {status, msg}}
{:error, %DocRaptor.Error{} = err} ->
{:error, err}
endPitfalls
- Localhost: DocRaptor cannot fetch private hosts. Prefer inlined
document_content(CSS/assets included). Relative URLs needprince_options.baseurlor absolute URLs. - Test mode: client default
test: true→ watermarked PDF / Excel capped at 20 rows / hosted limited to 5 downloads and 1-day expiry. Production needstest: falseon client or per document. - JavaScript: off by default — see engine table above.
- Prince watermarks (
@prince-overlay/flow): DocRaptor’s tutorials usetest: falsefor these samples (their public test key / test mode is not sufficient for that tutorial path). - Sync timeout: ~60s receive timeout, no Req retries — heavy docs → async.
- Hosted: paid add-on; expire with
DocRaptor.expire/2when done.
Phoenix + background jobs
# config/runtime.exs
config :my_app, :docraptor_api_key, System.fetch_env!("DOCRAPTOR_API_KEY")
# request path — sync only for fast docs
api_key = Application.fetch_env!(:my_app, :docraptor_api_key)
{:ok, client} = DocRaptor.Client.new(api_key: api_key, test: false)
{:ok, %DocRaptor.Document{body: pdf, content_type: ct}} =
DocRaptor.create_doc(client, %{
name: "invoice-#{id}.pdf",
document_type: :pdf,
document_content: html
})
conn
|> put_resp_content_type(ct || "application/pdf")
|> send_resp(200, pdf)Slow docs — Oban-style worker (blocking await is OK inside the worker):
def perform(%Oban.Job{args: %{"html" => html, "name" => name}}) do
api_key = Application.fetch_env!(:my_app, :docraptor_api_key)
{:ok, client} = DocRaptor.Client.new(api_key: api_key, test: false)
with {:ok, %DocRaptor.AsyncJob{status_id: id}} <-
DocRaptor.create_async_doc(client, %{
name: name,
document_type: :pdf,
document_content: html
}),
{:ok, %DocRaptor.Document{body: pdf}} <-
DocRaptor.await_async_doc(client, id) do
# store pdf / upload to S3 / email — then :
:ok
end
endOr pass callback_url: "https://example.com/webhooks/docraptor" on create and
handle the webhook instead of awaiting.
Testing
Use a distinct Req.Test stub name per test module/process to avoid
cross-talk:
{:ok, client} =
DocRaptor.Client.new(
api_key: "test",
req_options: [plug: {Req.Test, __MODULE__.DocRaptorStub}]
)Minimal patterns
Sync PDF
{:ok, client} = DocRaptor.Client.new(api_key: System.fetch_env!("DOCRAPTOR_API_KEY"))
{:ok, %DocRaptor.Document{body: pdf, num_pages: pages}} =
DocRaptor.create_doc(client, %{
name: "invoice.pdf",
document_type: :pdf,
document_content: html
})JS + print media
DocRaptor.create_doc(client, %{
name: "chart.pdf",
document_type: :pdf,
document_content: html_with_scripts,
javascript: true,
prince_options: %{media: "print"}
})Async await
{:ok, %DocRaptor.AsyncJob{status_id: id}} =
DocRaptor.create_async_doc(client, attrs)
{:ok, %DocRaptor.Document{body: pdf}} =
DocRaptor.await_async_doc(client, id, interval_ms: 1_000, timeout_ms: 600_000)