Current section
Files
Jump to
Current section
Files
docs/usage.md
# Usage (for humans and agents)
Short reference for integrating this package. Prefer this page + the `DocRaptor`
moduledoc over guessing from HTTP examples on docraptor.com.
## Entrypoint
1. `{:ok, client} = DocRaptor.Client.new(api_key: key)` — `:test` defaults to `true`
2. Pick a create function (table below)
3. Match on `{:ok, result} | {:error, %DocRaptor.Error{}}` — **no bang APIs**, and
`%DocRaptor.Error{}` is **not** an exception (no `defexception`; do not `raise` it)
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`, `help`
- `callback_url` (async webhook)
- `hosted_download_limit`, `hosted_expires_at` (hosted)
- `prince_options` map — 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 bytes
```
`await_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 `POST`s 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.
```elixir
# 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})
end
```
## Errors
```elixir
%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`
```elixir
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}
end
```
## Pitfalls
- **Localhost**: DocRaptor cannot fetch private hosts. Prefer inlined
`document_content` (CSS/assets included). Relative URLs need
`prince_options.baseurl` or 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 needs
`test: false` on client or per document.
- **JavaScript**: off by default — see engine table above.
- **Prince watermarks** (`@prince-overlay` / `flow`): DocRaptor’s tutorials use
`test: false` for 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/2` when done.
## Phoenix + background jobs
```elixir
# 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**):
```elixir
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
end
```
Or 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:
```elixir
{:ok, client} =
DocRaptor.Client.new(
api_key: "test",
req_options: [plug: {Req.Test, __MODULE__.DocRaptorStub}]
)
```
## Minimal patterns
### Sync PDF
```elixir
{: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
```elixir
DocRaptor.create_doc(client, %{
name: "chart.pdf",
document_type: :pdf,
document_content: html_with_scripts,
javascript: true,
prince_options: %{media: "print"}
})
```
### Async await
```elixir
{: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)
```