> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sajn.se/llms.txt
> Use this file to discover all available pages before exploring further.

# Download documents

> Download the sealed PDF, the original, and the signing journal, and archive them automatically when a document is completed

In this guide, you download the files of a document: the sealed PDF, the unsigned original, and the signing journal. Then you archive them automatically when a document is completed.

## Before you begin

* Store your API key in the `SAJN_API_KEY` environment variable. To create a key, go to workspace settings in the sajn app, then **Utvecklare** (Developer) > **API-nycklar** (API keys).
* Have the ID of a document. To download the sealed PDF or the journal, the document must be `COMPLETED`.

## Choose a file type

A document has up to three files:

* `SIGNED`: the sealed PDF with every signature and the [signing certificate](/concepts/signing-certificate). Ready when the document is `COMPLETED` and sealed.
* `ORIGINAL`: the unsigned document. Ready in every status.
* `JOURNAL`: the signing journal, a separate PDF with every party, their identity and signing method, and every event with its timestamp and IP address. Ready shortly after the document is `COMPLETED`.

To get one file, call `GET /api/v1/documents/DOCUMENT_ID/files/TYPE`. To see which files are ready, call `GET /api/v1/documents/DOCUMENT_ID/files`, which lists only the files that are ready, each with its own URL.

## Download a file

<Steps>
  <Step title="Get a download URL">
    The endpoint returns a pre-signed URL, not the file. Request the URL:

    <CodeGroup>
      ```bash curl theme={null}
      curl https://app.sajn.se/api/v1/documents/DOCUMENT_ID/files/SIGNED \
        -H "Authorization: Bearer $SAJN_API_KEY" \
        -H "Sajn-Version: 2026-10"
      ```

      ```javascript Node.js theme={null}
      const documentId = "DOCUMENT_ID";

      const response = await fetch(
        `https://app.sajn.se/api/v1/documents/${documentId}/files/SIGNED`,
        {
          headers: {
            Authorization: `Bearer ${process.env.SAJN_API_KEY}`,
            "Sajn-Version": "2026-10",
          },
        },
      );
      const { url } = await response.json();
      ```

      ```python Python theme={null}
      import os

      import requests

      document_id = "DOCUMENT_ID"

      response = requests.get(
          f"https://app.sajn.se/api/v1/documents/{document_id}/files/SIGNED",
          headers={
              "Authorization": f"Bearer {os.environ['SAJN_API_KEY']}",
              "Sajn-Version": "2026-10",
          },
      )
      response.raise_for_status()
      url = response.json()["url"]
      ```
    </CodeGroup>

    Replace `DOCUMENT_ID` with the document ID. The response is similar to the following:

    ```json theme={null}
    {
      "type": "SIGNED",
      "url": "https://files.sajn.se/documents/cm4k2x9p10001abcd1234efgh/sealed.pdf?Expires=1790845200&Signature=Qm9vZ3Vz&Key-Pair-Id=K2EXAMPLE",
      "expiresAt": "2026-10-02T10:45:00.000Z"
    }
    ```

    The URL is valid for 15 minutes, until `expiresAt`. Every URL that the API returns counts as a download: it's recorded in the document's audit log and fires the `security.document_downloaded` webhook event.
  </Step>

  <Step title="Fetch the file">
    Fetch the URL without the `Authorization` header, and save the response body:

    <CodeGroup>
      ```bash curl theme={null}
      curl -L "FILE_URL" -o signed-document.pdf
      ```

      ```javascript Node.js theme={null}
      import { writeFile } from "node:fs/promises";

      const file = await fetch(url);
      await writeFile("signed-document.pdf", Buffer.from(await file.arrayBuffer()));
      ```

      ```python Python theme={null}
      file = requests.get(url)
      file.raise_for_status()
      with open("signed-document.pdf", "wb") as output:
          output.write(file.content)
      ```
    </CodeGroup>

    Replace `FILE_URL` with the `url` from the previous step. The file is a PDF file. If the URL has expired, request a new one.
  </Step>
</Steps>

## Archive documents when they're completed

We recommend downloading each document as soon as it's completed, so your archive doesn't depend on polling. Subscribe a webhook endpoint to `document.completed`, which fires when the sealed PDF is ready, and download the files in the handler. The event's `data.object` is the document, so `data.object.id` is the document ID.

The following Express handler verifies the delivery's signature, answers right away, and then archives the sealed PDF and the journal:

```javascript theme={null}
import crypto from "node:crypto";
import { writeFile } from "node:fs/promises";

import express from "express";

const app = express();

const isSignedBySajn = (headers, rawBody) => {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  if (!id || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const key = Buffer.from(process.env.SAJN_WEBHOOK_SECRET.replace(/^whsec_/, ""), "base64");
  const expected = crypto
    .createHmac("sha256", key)
    .update(`${id}.${timestamp}.${rawBody}`)
    .digest();

  return String(headers["webhook-signature"] ?? "")
    .split(" ")
    .some((entry) => {
      const [version, signature] = entry.split(",");
      const received = Buffer.from(signature ?? "", "base64");
      return (
        version === "v1" &&
        received.length === expected.length &&
        crypto.timingSafeEqual(received, expected)
      );
    });
};

const download = async (documentId, type) => {
  const response = await fetch(
    `https://app.sajn.se/api/v1/documents/${documentId}/files/${type}`,
    { headers: { Authorization: `Bearer ${process.env.SAJN_API_KEY}`, "Sajn-Version": "2026-10" } },
  );
  if (!response.ok) {
    const error = await response.json();
    throw new Error(`${type}: ${error.code} (request ${error.requestId})`);
  }
  const { url } = await response.json();
  return Buffer.from(await (await fetch(url)).arrayBuffer());
};

app.post("/webhooks/sajn", express.raw({ type: "application/json" }), async (req, res) => {
  if (!isSignedBySajn(req.headers, req.body)) {
    return res.status(401).end();
  }
  res.status(200).end();

  const event = JSON.parse(req.body);
  if (event.type !== "document.completed") return;

  const document = event.data.object;
  const name = document.externalId ?? document.id;
  await writeFile(`archive/${name}.pdf`, await download(document.id, "SIGNED"));
  await writeFile(`archive/${name}-journal.pdf`, await download(document.id, "JOURNAL"));
});

app.listen(3000);
```

The handler reads the following environment variables:

* `SAJN_API_KEY`: your API key.
* `SAJN_WEBHOOK_SECRET`: the webhook's `whsec_` secret, returned when you create the webhook or rotate its secret.

The journal is generated after the sealed PDF, so in production, retry the `JOURNAL` download when it returns `409 INVALID_STATE`. A delivery can arrive more than once. Deduplicate on the event's `id`, or make the archive write idempotent, as the preceding example does by writing to a fixed filename. For more information, see [Verify webhook signatures](/webhooks/verify-signatures) and [Delivery and retries](/webhooks/delivery-and-retries).

To catch up on documents that completed while your endpoint was down, see [Sync all completed documents nightly](/guides/recipes/nightly-sync).

## Handle errors

Branch on the error `code`:

* `404 NOT_FOUND`, with `resource` set to `document`: the document doesn't exist, or your key can't see it.
* `409 INVALID_STATE`: the file isn't ready yet, for example `SIGNED` before the document is completed.

```json theme={null}
{
  "code": "INVALID_STATE",
  "message": "The document has no SIGNED file yet",
  "userMessage": "Filen (SIGNED) finns inte för det här dokumentet.",
  "requestId": "req_V1StGXR8Z5jdHi6BmyT2"
}
```

sajn generates the journal after it seals the document, so `JOURNAL` can return this error for a short time after `document.completed`. Retry it after a few seconds. For the error shape and every code, see [Errors](/api-fundamentals/errors).

## Next steps

<CardGroup cols={2}>
  <Card title="Nightly sync" icon="moon" href="/guides/recipes/nightly-sync">
    Archive every completed document on a schedule.
  </Card>

  <Card title="Signing certificate" icon="certificate" href="/concepts/signing-certificate">
    Learn what the sealed PDF proves.
  </Card>

  <Card title="Webhooks" icon="webhook" href="/webhooks/overview">
    Get notified when a document is completed.
  </Card>

  <Card title="Get a document file" icon="code" href="/api-reference/get-a-document-file">
    See the endpoint in the API reference.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.