> ## 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.

# Place fields on a PDF

> Put signature boxes, party inputs, and fixed text at exact positions on an uploaded PDF, and read the layout back

In this guide, you place boxes on the pages of an uploaded PDF: a signature that a party draws when signing, an input that a party fills in, and fixed text that you stamp. Then you move a box and read the layout back.

A `PDF` field holds one uploaded PDF. The boxes on its pages are in `fieldMeta.placedFields`, an array where each entry is one box, drawn in array order.

## 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 a `DRAFT` document with its parties, and each party's `id`. For more information, see [Create a document](/guides/documents/create-document).
* Upload the PDF file and keep the storage `key`. For more information, see [Upload files](/guides/documents/file-uploads).

## Coordinates

Boxes use the PDF's own coordinate model:

* `page`: the 0-based page index.
* `rect`: `x`, `y`, `width`, and `height` in PDF points (1/72 inch). The origin is the bottom-left corner of the page, and `y` grows upward.
* `pageWidth` and `pageHeight`: optional. The page size that `rect` is measured on. A4 portrait is `595.28` by `841.89`. If you send them, sajn scales `rect` to the real page. If you leave them out, sajn reads them from the PDF.
* `pageRotation`: optional. `0`, `90`, `180`, or `270`. If you leave it out, sajn reads it from the PDF.

If your own model uses fractions measured from the top-left corner, convert them with `x = xFraction * pageWidth` and `y = pageHeight - (yFraction + heightFraction) * pageHeight`.

## Box kinds

Each box has an `id` that's unique in the field, and a `kind`:

* `INPUT` with `inputType` `SIGNATURE` or `INITIALS`: a mark that the party in `partyId` draws when signing. sajn seals the mark into the PDF at this position. The party must have the `SIGNER` role.
* `INPUT` with `inputType` `TEXT`, `DATE`, `CHECKBOX`, or `SELECT`: a value that the party in `partyId` fills in before signing. Add `options` for `SELECT` and `"multiline": true` for long text. If `required` is `true`, the party can't sign until it's filled in. Without a `partyId`, you fill the box in yourself.
* `STATIC`: fixed text that you stamp on the page. `content` is HTML, and `style` takes `fontSize` (6 to 72), `color` (hex), `bold`, `italic`, and `align` (`LEFT`, `CENTER`, or `RIGHT`).

Every enum value in a box is uppercase; a lowercase value, such as `input`, fails with `400 VALIDATION_FAILED`. To fill in an `INPUT` box through the API, give it a `key`. Keys can contain lowercase letters, digits, hyphens, and underscores.

## Place the boxes

<Steps>
  <Step title="Create the PDF field with its boxes">
    Send a `POST` request to `/api/v1/documents/DOCUMENT_ID/fields` with the PDF field in a `fields` array. The following request places a signature on page 3, a start-date input on page 1, and a reference number in the top-right corner of page 1:

    <CodeGroup>
      ```bash curl theme={null}
      curl -X POST https://app.sajn.se/api/v1/documents/DOCUMENT_ID/fields \
        -H "Authorization: Bearer $SAJN_API_KEY" \
        -H "Sajn-Version: 2026-10" \
        -H "Content-Type: application/json" \
        -d '{
          "fields": [
            {
              "type": "PDF",
              "position": 0,
              "fieldMeta": {
                "type": "PDF",
                "value": "STORAGE_KEY",
                "placedFields": [
                  { "id": "signature", "kind": "INPUT", "inputType": "SIGNATURE",
                    "page": 2, "rect": { "x": 72, "y": 96, "width": 200, "height": 48 },
                    "partyId": "PARTY_ID", "required": true },
                  { "id": "start-date", "key": "start-date", "kind": "INPUT", "inputType": "DATE",
                    "label": "Start date", "page": 0,
                    "rect": { "x": 320, "y": 640, "width": 140, "height": 22 },
                    "partyId": "PARTY_ID", "required": true },
                  { "id": "reference", "kind": "STATIC", "content": "<p>Ref: HR-2026-0142</p>",
                    "page": 0, "rect": { "x": 400, "y": 780, "width": 150, "height": 18 },
                    "style": { "fontSize": 9, "align": "RIGHT" } }
                ]
              }
            }
          ]
        }'
      ```

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

      const response = await fetch(`https://app.sajn.se/api/v1/documents/${documentId}/fields`, {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.SAJN_API_KEY}`,
          "Sajn-Version": "2026-10",
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          fields: [
            {
              type: "PDF",
              position: 0,
              fieldMeta: {
                type: "PDF",
                value: "STORAGE_KEY",
                placedFields: [
                  { id: "signature", kind: "INPUT", inputType: "SIGNATURE",
                    page: 2, rect: { x: 72, y: 96, width: 200, height: 48 },
                    partyId, required: true },
                  { id: "start-date", key: "start-date", kind: "INPUT", inputType: "DATE",
                    label: "Start date", page: 0,
                    rect: { x: 320, y: 640, width: 140, height: 22 },
                    partyId, required: true },
                  { id: "reference", kind: "STATIC", content: "<p>Ref: HR-2026-0142</p>",
                    page: 0, rect: { x: 400, y: 780, width: 150, height: 18 },
                    style: { fontSize: 9, align: "RIGHT" } },
                ],
              },
            },
          ],
        }),
      });
      const body = await response.json();
      if (!response.ok) {
        // VALIDATION_FAILED lists every invalid box in `issues`.
        console.error(body.code, body.requestId, body.issues);
        throw new Error(body.message);
      }
      console.log(body.data[0].id);
      ```

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

      import requests

      document_id = "DOCUMENT_ID"
      party_id = "PARTY_ID"

      response = requests.post(
          f"https://app.sajn.se/api/v1/documents/{document_id}/fields",
          headers={
              "Authorization": f"Bearer {os.environ['SAJN_API_KEY']}",
              "Sajn-Version": "2026-10",
          },
          json={
              "fields": [
                  {
                      "type": "PDF",
                      "position": 0,
                      "fieldMeta": {
                          "type": "PDF",
                          "value": "STORAGE_KEY",
                          "placedFields": [
                              {"id": "signature", "kind": "INPUT", "inputType": "SIGNATURE",
                               "page": 2,
                               "rect": {"x": 72, "y": 96, "width": 200, "height": 48},
                               "partyId": party_id, "required": True},
                              {"id": "start-date", "key": "start-date", "kind": "INPUT",
                               "inputType": "DATE", "label": "Start date", "page": 0,
                               "rect": {"x": 320, "y": 640, "width": 140, "height": 22},
                               "partyId": party_id, "required": True},
                              {"id": "reference", "kind": "STATIC",
                               "content": "<p>Ref: HR-2026-0142</p>", "page": 0,
                               "rect": {"x": 400, "y": 780, "width": 150, "height": 18},
                               "style": {"fontSize": 9, "align": "RIGHT"}},
                          ],
                      },
                  },
              ],
          },
      )
      body = response.json()
      if not response.ok:
          # VALIDATION_FAILED lists every invalid box in `issues`.
          print(body["code"], body["requestId"], body.get("issues"))
          raise RuntimeError(body["message"])
      print(body["data"][0]["id"])
      ```
    </CodeGroup>

    Replace the following:

    * `DOCUMENT_ID`: the ID of the draft document.
    * `STORAGE_KEY`: the `key` from the file upload.
    * `PARTY_ID`: the `id` of the signing party.

    The response has the created PDF field in `data`. Store its `id` for the next step.

    sajn checks every box before it writes anything. The request fails with `400 VALIDATION_FAILED`, and writes nothing, when a box's `page` doesn't exist, its `rect` doesn't fit on the page, its `partyId` isn't a party on the document, or it's a signature or initials box for a party without the `SIGNER` role.
  </Step>

  <Step title="Move, add, or remove single boxes">
    To change boxes without sending the whole field again, send a `PATCH` request to `/api/v1/documents/DOCUMENT_ID/fields/FIELD_ID/placed-fields`:

    ```bash theme={null}
    curl -X PATCH https://app.sajn.se/api/v1/documents/DOCUMENT_ID/fields/FIELD_ID/placed-fields \
      -H "Authorization: Bearer $SAJN_API_KEY" \
      -H "Sajn-Version: 2026-10" \
      -H "Content-Type: application/json" \
      -d '{
        "upsert": [
          { "id": "signature", "kind": "INPUT", "inputType": "SIGNATURE",
            "page": 3, "rect": { "x": 72, "y": 120, "width": 200, "height": 48 },
            "partyId": "PARTY_ID" }
        ],
        "remove": ["reference"]
      }'
    ```

    Replace `FIELD_ID` with the PDF field's `id`. The request works as follows:

    * A box in `upsert` with an existing `id` replaces that box and keeps its drawing order. A box with a new `id` is added on top.
    * `remove` lists the IDs of boxes to delete. Boxes you don't mention stay as they are.
    * The change is atomic: if any box is invalid, nothing is written, and `issues` lists every problem.

    The response is the PDF field with its full `fieldMeta` after the change.
  </Step>

  <Step title="Read the layout back">
    List the document's fields:

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

    The response lists every field in position order in `data`, and the PDF field has the `placedFields` you sent, with `pageWidth`, `pageHeight`, and `pageRotation` filled in. The following example leaves out most boxes:

    ```json theme={null}
    {
      "data": [
        {
          "id": "cm4k2xd5s0005abcd7890uvwx",
          "documentId": "cm4k2x9p10001abcd1234efgh",
          "templateId": null,
          "type": "PDF",
          "position": 0,
          "fieldMeta": {
            "type": "PDF",
            "value": "f/cm4k2xz0a0000abcd0000orgx/cm4k2xf3u0007abcd2468cdef/contract.pdf",
            "numPages": 4,
            "placedFields": [
              {
                "id": "signature",
                "kind": "INPUT",
                "inputType": "SIGNATURE",
                "page": 3,
                "rect": { "x": 72, "y": 120, "width": 200, "height": 48 },
                "pageWidth": 595.28,
                "pageHeight": 841.89,
                "partyId": "cm4k2xa3f0002abcd5678ijkl"
              }
            ]
          },
          "createdAt": "2026-10-01T09:10:00.000Z",
          "updatedAt": "2026-10-01T09:12:00.000Z"
        }
      ]
    }
    ```

    `GET /api/v1/documents/DOCUMENT_ID?expand=fields` returns the same fields in the document's `fields` array.
  </Step>
</Steps>

## Fill in boxes and PDF form fields

To fill in an `INPUT` box that has no `partyId`, use [Fill in values on a document](/api-reference/fill-in-values-on-a-document) with the box's `key`. The same endpoint fills in the uploaded PDF's own form fields (AcroForm fields), which sajn lists in `fieldMeta.formFields` with an uppercase `type`: `TEXT`, `CHECKBOX`, `DROPDOWN`, or `RADIO`. To see every fillable key, call [List the fillable values on a document](/api-reference/list-the-fillable-values-on-a-document). The values are in `data`: values with `kind` set to `PDF_PLACED` are boxes, and values with `kind` set to `PDF_ACROFORM` are the PDF's own form fields.

## Use placements on templates

The same requests work on templates, through `/api/v1/templates/TEMPLATE_ID/fields` and `/api/v1/templates/TEMPLATE_ID/fields/FIELD_ID/placed-fields`. On a template, `partyId` refers to a template party. A document created from the template gets the boxes, mapped to the matching document parties.

## Handle errors

A placement error lists every invalid box in `issues`, with its path:

```json theme={null}
{
  "code": "VALIDATION_FAILED",
  "message": "The box must fit on the 595.28 × 841.89 pt page, measured from its bottom-left corner, but it spans x 500 to 700 and y 96 to 144.",
  "userMessage": "Förfrågan innehåller ogiltiga värden.",
  "requestId": "req_V1StGXR8Z5jdHi6BmyT2",
  "issues": [
    {
      "path": "fields.0.fieldMeta.placedFields.0.rect",
      "code": "INVALID_VALUE",
      "message": "The box must fit on the 595.28 × 841.89 pt page, measured from its bottom-left corner, but it spans x 500 to 700 and y 96 to 144."
    }
  ]
}
```

* `400 VALIDATION_FAILED`: a box is invalid, uses a lowercase enum value such as `signature`, or uses a 2026-09 name such as `signerId` instead of `partyId`.
* `409 INVALID_STATE`: the document isn't a `DRAFT`, or another edit changed the field while the request ran. In the second case, send the request again.

For the error shape and every code, see [Errors](/api-fundamentals/errors).

## Next steps

<CardGroup cols={2}>
  <Card title="Send for signing" icon="paper-plane" href="/guides/documents/send-for-signing">
    Send the document to its parties.
  </Card>

  <Card title="Fields that parties fill in" icon="pen-field" href="/guides/fields/signer-fields">
    Add form inputs outside the PDF.
  </Card>

  <Card title="Add, update, or remove boxes" icon="code" href="/api-reference/add-update-or-remove-boxes-on-a-pdf-field">
    See the placed-fields endpoint in the API reference.
  </Card>

  <Card title="Fields" icon="lightbulb" href="/concepts/fields">
    Learn how the field model works.
  </Card>
</CardGroup>


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