---
title: "Download the executed PDF"
method: GET
path: "/envelopes/{envelopeId}/document"
tags: ["Envelopes"]
---

# Download the executed PDF

`GET /envelopes/{envelopeId}/document`

## Request

- Base URL: `https://inkseal.net/api/v1`
- URL: `https://inkseal.net/api/v1/envelopes/{envelopeId}/document`
- Auth: HTTP bearer

**This route does not return JSON on success.** It streams the executed PDF itself, proxied through the API while the credential is live: `200` with `Content-Type: application/pdf` and `Content-Disposition: attachment`, the filename derived from the original upload with a `_signed` suffix. There is no redirect and no presigned URL; an earlier version answered with a signed R2 link, and that link kept working after the credential was revoked, which is why the bytes now move only under the live credential.

The evidence that used to share that JSON body rides along as response headers: `X-Document-Hash` (SHA-256 of the executed PDF) and `X-Document-Completed-At`.

Ranged requests are supported: a satisfiable `Range` answers `206` with `Content-Range`, and an unsatisfiable one answers `416`. `ETag` and `Content-Length` are set from the stored object.

**Only a `completed` envelope has a downloadable PDF.** Every other state (`draft`, `sending`, `pending`, `completing`, `declined`, `voided`, `expired`), and a `completed` envelope whose stored key is missing, answers 400 `not_completed`.

Surprising but deliberate: this operation costs `documents:read`, not `envelopes:read`, because it returns document bytes and is priced as a document read.

## Headers

- `Range` string

## Response `200`

The executed PDF, streamed.

## Other responses

- `206`: The requested byte range of the executed PDF.
- `400`: `not_completed`: the envelope has no executed PDF yet. Only a `completed` envelope does.
- `401`: The credential is missing, malformed, unknown, expired, revoked, or past its rotation grace window. Code: `unauthorized`. Every authentication failure is deliberately indistinguishable.
- `403`: The credential is valid but may not do this. Codes: `insufficient_scope` (the route's scopes are not all held; `details` names `requiredScopes` and `missingScopes`), `forbidden` (the capability check on the resolved resource failed), `scope_escalation` (a grant wider than the caller's own was requested).
- `404`: No such envelope in this workspace (JSON `not_found`), or the envelope's stored object is gone from storage (empty body).
- `416`: The `Range` header was malformed, asked for more than one range, or named a range the object cannot satisfy. The body is empty.
- `429`: A limit was reached. Codes: `rate_limited` (the credential's per-bucket HTTP limit, or the per-workspace detection throttle), `workspace_limit_exceeded` (a workspace content or send limit; `details.limit` names it), `daily_budget_exceeded` (the workspace's daily detection budget). The four rate-limit headers are sent on this response and on no successful one, so a client cannot read its remaining budget without being refused. Retrying after `Retry-After` is always safe: a rate-limit refusal happens before the handler runs.
- `503`: A dependency the request needs is unreachable and nothing was changed. Codes: `queue_unavailable` (a background job could not be enqueued; retry the call), `billing_unavailable` (Stripe would not open a checkout session; nothing was charged, retry the call), `service_unavailable` (the rate-limit store is down, so the request was refused before running).

---

[API](https://skmtc.dev/inkseal/apis/inkseal-api.md) · [All operations](https://skmtc.dev/inkseal/apis/inkseal-api/llms.txt) · [OpenAPI document](https://skmtc.dev/inkseal/apis/inkseal-api/revisions/c440c981d578?raw)
