# Linkjar API

Publish documents and pages as tracked links, make personal links, change link settings, and read who opened them, form responses and signatures. JSON over HTTPS.

- Base URL: `https://linkjar.com/api/v1/`
- OpenAPI 3.1: https://linkjar.com/api/v1/openapi.json
- CLI: https://linkjar.com/docs/cli.md

## Authentication

Send `Authorization: Bearer <token>`. Either:

- an API key (starts with `lj_`) from Settings → Claude, CLI & API, for servers and CI; or
- an OAuth token from `linkjar login` (OAuth 2.1 with PKCE; discovery at https://linkjar.com/.well-known/oauth-authorization-server).

Read endpoints need the `read` scope, everything else `write`. API keys have both.

## Errors

Errors are JSON: `{"error": "message"}` (403 adds `upgrade_url`).

| Status | Meaning |
| --- | --- |
| 400 | Something in the request is wrong; `error` says what. |
| 401 | Missing, invalid or revoked token. |
| 403 | The feature or limit needs a paid plan. The body includes `upgrade_url`. |
| 404 | No such document (or not in your workspace). |
| 409 | Not ready yet (a signed PDF still being made). |
| 429 | Rate limited: publishing and updates are capped at 30 an hour per user. |
| 500 | Our fault. Try again; it is logged. |

`ref` in a path is a document's link, slug or id.

## Account

### Who the token belongs to `GET /api/v1/me`

The signed-in user and the workspace the API acts on. A quick way to check a key works.

```bash
curl https://linkjar.com/api/v1/me \
  -H "Authorization: Bearer $LINKJAR_API_KEY"
```

Response `200`:

```json
{
  "user": {
    "email": "you@acme.com"
  },
  "workspace": {
    "uuid": "0b5f…",
    "name": "Acme",
    "plan": "Pro"
  },
  "app_url": "{base}"
}
```

## Documents

### List documents `GET /api/v1/documents`

Published documents in the workspace, newest first.

Parameters:

- `query` (query, string): Only titles containing this text.
- `limit` (query, integer): 1–100, default 20.

```bash
curl https://linkjar.com/api/v1/documents \
  -H "Authorization: Bearer $LINKJAR_API_KEY"
```

Response `200`:

```json
{
  "documents": [
    {
      "title": "Acme proposal",
      "link": "https://pdflink.to/acme-proposal/",
      "slug": "acme-proposal",
      "kind": "html",
      "views": 3,
      "created": "2026-10-15"
    }
  ]
}
```

Errors: `400` limit is not a number.

### Publish a document `POST /api/v1/documents`

Publishes a file or an HTML page and returns its link. Send either JSON with `html` (one complete, self-contained HTML document) or multipart/form-data with a `file`: PDF, Word, PowerPoint, Excel, .html/.htm, or a .zip of a website with index.html at its top. Office files are converted to PDF first (`status` is `Converting` for a minute or two; the link works once it's done). Every upload is scanned; a page held for review has `review_status: "held"` until approved. Settings that need a paid plan are checked before anything is created.

Body fields:

- `title` (string): Title shown to viewers.
- `html` (string): The page (JSON only), up to 5 MB.
- `password` (string): Visitors need this password to open the link (4+ characters).
- `require_email` (boolean): Ask viewers for their email first (Pro).
- `allowed_email_domains` (string): Only accept emails at these comma-separated domains, e.g. "acme.com, acme.co.uk".
- `expires_on` (string): YYYY-MM-DD after which the link stops working (paid plans).
- `cta_label` (string): Call-to-action button text, e.g. "Book a call".
- `cta_url` (string): Where the button goes (full https:// URL).

Multipart fields:

- `file`: The file to publish.
- `title`: Optional. Defaults to the page <title> or the file name.
- `password`: Visitors need this password to open the link (4+ characters).
- `require_email`: Ask viewers for their email first (Pro).
- `allowed_email_domains`: Only accept emails at these comma-separated domains, e.g. "acme.com, acme.co.uk".
- `expires_on`: YYYY-MM-DD after which the link stops working (paid plans).
- `cta_label`: Call-to-action button text, e.g. "Book a call".
- `cta_url`: Where the button goes (full https:// URL).

```bash
curl -X POST https://linkjar.com/api/v1/documents \
  -H "Authorization: Bearer $LINKJAR_API_KEY" \
  -F file=@deck.pdf \
  -F title="Q4 deck" -F password=hunter22
```

Response `201`:

```json
{
  "title": "Acme proposal",
  "link": "https://pdflink.to/acme-proposal/",
  "slug": "acme-proposal",
  "uuid": "6f1c2d9e-8a7b-4c3d-9e2f-1a2b3c4d5e6f",
  "kind": "html",
  "status": "Published",
  "review_status": null,
  "views": 3,
  "created": "2026-10-15T09:00:00+00:00",
  "settings_url": "{base}/document/6f1c2d9e-8a7b-4c3d-9e2f-1a2b3c4d5e6f/update/",
  "settings": {
    "password": false,
    "require_email": false,
    "allowed_email_domains": "",
    "expires_at": null,
    "cta_label": "Book a call",
    "cta_url": "https://cal.com/you/intro"
  },
  "warnings": [],
  "message": "Published \"Acme proposal\": https://pdflink.to/acme-proposal/"
}
```

Errors: `400` No html or file, an unsupported file type, or a bad setting.; `403` Document limit reached, or a setting needs a paid plan (`upgrade_url` included).; `429` More than 30 publishes in an hour.

### Get a document `GET /api/v1/documents/{ref}`

A document with its link, status, view count and link settings.

Parameters:

- `ref` (path, string, required): The document: its link, slug (the part after the domain) or id.

```bash
curl https://linkjar.com/api/v1/documents/acme-proposal \
  -H "Authorization: Bearer $LINKJAR_API_KEY"
```

Response `200`:

```json
{
  "title": "Acme proposal",
  "link": "https://pdflink.to/acme-proposal/",
  "slug": "acme-proposal",
  "uuid": "6f1c2d9e-8a7b-4c3d-9e2f-1a2b3c4d5e6f",
  "kind": "html",
  "status": "Published",
  "review_status": null,
  "views": 3,
  "created": "2026-10-15T09:00:00+00:00",
  "settings_url": "{base}/document/6f1c2d9e-8a7b-4c3d-9e2f-1a2b3c4d5e6f/update/",
  "settings": {
    "password": false,
    "require_email": false,
    "allowed_email_domains": "",
    "expires_at": null,
    "cta_label": "Book a call",
    "cta_url": "https://cal.com/you/intro"
  }
}
```

Errors: `404` No such document in your workspace.

### Change link settings `PATCH /api/v1/documents/{ref}`

Only the fields you send change. Use the `remove_*` flags to turn a setting off.

Parameters:

- `ref` (path, string, required): The document: its link, slug (the part after the domain) or id.

Body fields:

- `title` (string): New title.
- `password` (string): Visitors need this password to open the link (4+ characters).
- `require_email` (boolean): Ask viewers for their email first (Pro).
- `allowed_email_domains` (string): Only accept emails at these comma-separated domains, e.g. "acme.com, acme.co.uk".
- `expires_on` (string): YYYY-MM-DD after which the link stops working (paid plans).
- `cta_label` (string): Call-to-action button text, e.g. "Book a call".
- `cta_url` (string): Where the button goes (full https:// URL).
- `remove_password` (boolean): Turn password protection off.
- `remove_expiry` (boolean): Remove the expiry date.
- `remove_cta` (boolean): Remove the button.

```bash
curl -X PATCH https://linkjar.com/api/v1/documents/acme-proposal \
  -H "Authorization: Bearer $LINKJAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"password": "hunter22", "expires_on": "2026-12-31"}'
```

Response `200`:

```json
{
  "link": "https://pdflink.to/acme-proposal/",
  "changed": [
    "password",
    "expiry"
  ],
  "message": "Updated https://pdflink.to/acme-proposal/: password, expiry."
}
```

Errors: `400` A bad value, e.g. expires_on not a date.; `403` The setting needs a paid plan.; `404` No such document.

### Replace what a link shows `PUT /api/v1/documents/{ref}/content`

Swaps in a new version and keeps the same link, so everyone who has it sees the update. Send JSON `html` for web pages, or a multipart `file` of the same kind (a PDF for a PDF, HTML/zip for a page).

Parameters:

- `ref` (path, string, required): The document: its link, slug (the part after the domain) or id.

Multipart fields:

- `file`: The new version.
- `title`: Optional new title.

```bash
curl -X PUT https://linkjar.com/api/v1/documents/acme-proposal/content \
  -H "Authorization: Bearer $LINKJAR_API_KEY" \
  -F file=@deck.pdf
```

Response `200`:

```json
{
  "link": "https://pdflink.to/acme-proposal/",
  "message": "Updated https://pdflink.to/acme-proposal/ (same link).",
  "warnings": []
}
```

Errors: `400` A different kind of file than the original.; `403` Update limit reached on the free plan.; `404` No such document.; `429` Too many updates in an hour.

## Tracking

### Who opened it `GET /api/v1/documents/{ref}/activity`

Recent views: who (when they used a personal link or the email gate), when, where and on what device. On Pro also time spent, how far they got (`completion_pct`), clicks (`engagement`) and, for web pages, the headings they scrolled to (`sections`).

Parameters:

- `ref` (path, string, required): The document: its link, slug (the part after the domain) or id.
- `days` (query, integer): Look back this many days (default 90).
- `limit` (query, integer): Most recent views to return, 1–50 (default 20).

```bash
curl https://linkjar.com/api/v1/documents/acme-proposal/activity \
  -H "Authorization: Bearer $LINKJAR_API_KEY"
```

Response `200`:

```json
{
  "title": "Acme proposal",
  "total_views": 3,
  "views": [
    {
      "when": "2026-10-15T09:14:00+00:00",
      "who": "Sam Lee",
      "where": "Chicago, Illinois, United States",
      "device": "Chrome, Mac",
      "time": "3m 10s",
      "completion_pct": 94,
      "engagement": "read 94% of the page; clicked Book a call",
      "sections": [
        "Summary",
        "Pricing"
      ]
    }
  ],
  "message": "\"Acme proposal\": 3 views in total.\n- 2026-10-15 09:14: Sam Lee (Chicago, …)"
}
```

Errors: `404` No such document.

### Form responses and signatures `GET /api/v1/documents/{ref}/responses`

What people filled in and submitted on a fillable PDF, newest first. `answers` lists them in the PDF's order with readable labels (the owner's names for the fields, else ones read from the PDF); `fields` has the same answers keyed by the PDF's own field names. Signed responses have `signed: true`, the signer, an audit trail (time, IP, location, device, consent, document hashes) and a `signed_pdf` download URL. Taking submissions is on paid plans; past responses stay readable.

Parameters:

- `ref` (path, string, required): The document: its link, slug (the part after the domain) or id.
- `limit` (query, integer): 1–200, default 50.

```bash
curl https://linkjar.com/api/v1/documents/acme-proposal/responses \
  -H "Authorization: Bearer $LINKJAR_API_KEY"
```

Response `200`:

```json
{
  "title": "Service agreement",
  "total": 1,
  "responses": [
    {
      "id": "0d6c2b1e-6a1f-4f0e-9a51-1e2b8c7d9f00",
      "when": "2026-10-15T18:00:00+00:00",
      "who": "Sam Lee",
      "email": "sam@acme.com",
      "fields": {
        "full_name": "Sam Lee",
        "agree": "Yes",
        "signature": {
          "type": "signature",
          "signer_name": "Sam Lee",
          "method": "draw"
        }
      },
      "answers": [
        {
          "name": "full_name",
          "label": "Full name",
          "value": "Sam Lee"
        },
        {
          "name": "agree",
          "label": "I agree to the terms",
          "value": "Yes"
        },
        {
          "name": "signature",
          "label": "Signature",
          "value": {
            "type": "signature",
            "signer_name": "Sam Lee",
            "method": "draw"
          }
        }
      ],
      "signed": true,
      "signer_name": "Sam Lee",
      "signed_pdf_ready": true,
      "audit": {
        "signed_at": "2026-10-15T18:00:00+00:00",
        "ip": "203.0.113.9",
        "location": "Chicago, Illinois, United States"
      },
      "signed_pdf": "{base}/api/v1/documents/service-agreement/responses/0d6c2b1e-6a1f-4f0e-9a51-1e2b8c7d9f00/signed.pdf"
    }
  ],
  "message": "\"Service agreement\": 1 response."
}
```

Errors: `404` No such document.

### Download a signed PDF `GET /api/v1/documents/{ref}/responses/{response_uuid}/signed.pdf`

The signed copy of a response: the answers and signature drawn in, plus a signature certificate page.

Parameters:

- `ref` (path, string, required): The document: its link, slug (the part after the domain) or id.
- `response_uuid` (path, string, required): The response `id` from the responses endpoint.

```bash
curl -o signed.pdf https://linkjar.com/api/v1/documents/acme-proposal/responses/0d6c2b1e-6a1f-4f0e-9a51-1e2b8c7d9f00/signed.pdf \
  -H "Authorization: Bearer $LINKJAR_API_KEY"
```

Response `200`: the file (`application/pdf`).

Errors: `404` No signed response with that id.; `409` The signed copy is still being made; try again in a minute.

## Sharing

### Make a personal link `POST /api/v1/documents/{ref}/recipients`

A link for one person, so their views (and form responses) show up under their name.

Parameters:

- `ref` (path, string, required): The document: its link, slug (the part after the domain) or id.

Body fields:

- `name` (string): Their name.
- `email` (string): Their email. Give a name, an email or both.

```bash
curl -X POST https://linkjar.com/api/v1/documents/acme-proposal/recipients \
  -H "Authorization: Bearer $LINKJAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Sam Lee", "email": "sam@acme.com"}'
```

Response `201`:

```json
{
  "link": "https://pdflink.to/acme-proposal/?r=6r3cpddgxy",
  "message": "Link for Sam Lee: https://pdflink.to/acme-proposal/?r=6r3cpddgxy"
}
```

Errors: `400` Neither name nor email.; `404` No such document.

### Create a room `POST /api/v1/rooms`

Several documents behind one link, e.g. a deal room. Pro.

Body fields:

- `title` (string): Room title.
- `documents` (array): Links or slugs, in display order (up to 50).
- `description` (string): Optional text shown at the top.
- `require_email` (boolean): Ask visitors for their email once.

```bash
curl -X POST https://linkjar.com/api/v1/rooms \
  -H "Authorization: Bearer $LINKJAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title": "Acme deal room", "documents": ["acme-proposal", "case-study"], "require_email": true}'
```

Response `201`:

```json
{
  "link": "https://pdflink.to/r/acme-deal-room/",
  "message": "Room \"Acme deal room\" with 2 documents: https://pdflink.to/r/acme-deal-room/"
}
```

Errors: `400` `documents` is not a list.; `403` Rooms need Pro.; `404` One of the documents is not yours.
