Packages

Elixir client for the DocRaptor HTML-to-PDF/XLS API.

Current section

Files

Jump to
docraptor docs usage.md
Raw

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)
```