# REST API

The REST API does what the panel on the page does, from another system: a contract tool, a
workflow engine, an HR system or a script. It is about signing only, and it is not a general
Confluence API.

The global settings carry the same reference under **API access**, beside the one thing no
website can tell you: your site's own address.

## Before you start

You need three things:

1. **SignPlus installed** on your Confluence site.
2. **The API address.** **API access**, then **API address**. Atlassian creates it for each
   installation, so it is different on every site and cannot be guessed. Every endpoint below is
   a path added to it, such as `/v1/signings`.
3. **An Atlassian service account with an OAuth 2.0 credential.** SignPlus issues no API keys of
   its own. An organisation administrator creates the service account and its credential in
   Atlassian Administration, copies the `client_id` and `client_secret`, which are shown once,
   grants the credential one scope, `read:confluence-user`, and gives the service account the
   Confluence permissions it should have.
   [Atlassian's instructions](https://support.atlassian.com/user-management/docs/create-oauth-2-0-credential-for-service-accounts/).

### What the service account's permissions decide

SignPlus spends the token once against your site to find out whose it is, checks that account's
Confluence permissions on the page on every request, and **reads the page as that account**.
More scopes on the credential grant nothing extra.

| To | The account needs |
|---|---|
| Read a page's status, its signings, their people, a PDF or a signed document | **View** the page |
| Start a signing, cancel one, or export a PDF | **Edit** the page |
| Delete a finished signing | What the site's [deleting signings](../configure/#deleting-signings) setting allows |

Because the page is read as the service account, a signing holds what that account sees: a Jira
work items table lists what it may see, and an excerpt from a page it cannot open is left out.
Give it the access the document should reflect. A token from another Atlassian organisation is
refused by Atlassian itself.

## Your first signing

Fetch a token. The client id and secret travel in the body of a POST, read from the environment so
the secret is never written on a command line.

```bash
export SIGNPLUS_URL="https://513c7988-....hello.atlassian.net/x1/G7gth8r-Vw5-wMfGB7FGgW6AQ9A"
export CLIENT_ID=... CLIENT_SECRET=...

export SIGNPLUS_TOKEN=$(jq -n '{grant_type: "client_credentials", client_id: env.CLIENT_ID, client_secret: env.CLIENT_SECRET}' \
  | curl -s -X POST 'https://auth.atlassian.com/oauth/token' \
      -H 'Content-Type: application/json' --data @- \
  | jq -r .access_token)
```

The token lasts 60 minutes. A page id is in the page's address, or under **Page information**.

### 1. Ask where the page stands

```bash
curl -s "$SIGNPLUS_URL/v1/pages/123456/status" -H "Authorization: Bearer $SIGNPLUS_TOKEN"
```

```json
{ "pageId": "123456", "spaceId": "98765", "pageVersion": 4, "state": "ready-to-sign", "signings": [] }
```

A `401` means the token is wrong or has expired. A `403` means the service account cannot see that
page.

### 2. Start a signing

```bash
curl -s -X POST "$SIGNPLUS_URL/v1/signings" \
  -H "Authorization: Bearer $SIGNPLUS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "pageId": "123456",
    "signers": [
      { "email": "anna@example.com", "name": "Anna Tamm" },
      { "group": "finance-team" }
    ]
  }'
```

```json
{ "jobId": "7c1e9a0f3b2d4c5e6f7a8b9c", "status": "queued", "pageId": "123456",
  "statusUrl": "/v1/pages/123456/signing-jobs/7c1e9a0f3b2d4c5e6f7a8b9c" }
```

The answer is `202 Accepted`. The request has been checked against the space's settings, and the
signing is prepared in the background: the page is rendered, every member of the group is looked
up, and everybody is sent to Dokobit. That takes seconds for a few people and minutes for a group
of thousands.

### 3. Follow the job

```bash
curl -s "$SIGNPLUS_URL/v1/pages/123456/signing-jobs/7c1e9a0f3b2d4c5e6f7a8b9c" \
  -H "Authorization: Bearer $SIGNPLUS_TOKEN"
```

```json
{ "id": "7c1e9a0f3b2d4c5e6f7a8b9c", "status": "started", "reference": "a3f9c2d1...", "documentName": "Expense policy" }
```

`queued` and `running` mean it is being prepared. `started` means Dokobit has it and the
invitations are out; keep the `reference`, which is the signing's public id and what the other
endpoints take. `failed` carries the reason in `message`.

### 4. See who has not signed

```bash
curl -s "$SIGNPLUS_URL/v1/signings/a3f9c2d1.../signers?pageId=123456&status=pending" \
  -H "Authorization: Bearer $SIGNPLUS_TOKEN" | jq -r '.signers[] | [.name, .email] | @tsv'
```

<Aside type="note" title="Do not poll for completion">
A signing takes minutes or days. Configure the [completion webhook](../automation/#when-a-signing-completes)
instead, and SignPlus posts you the signed document when it exists.
</Aside>

## Authentication and limits

```
Authorization: Bearer <the access token>
```

That is the only credential. Every authentication failure answers the same `401` with the same
sentence, so the API cannot be used to find out which tokens are live. Basic authentication is
not supported. Revoke the credential in Atlassian Administration, and SignPlus stops accepting its
tokens within two minutes.

| Limit | |
|---|---|
| Rate | 60 requests per minute per account, answered with `429` |
| Request body | 2MB, answered with `413` |
| People on one signing | 25,000, after groups are expanded |
| People named one by one in a view restriction | 200. A group counts as one |
| People on one reply | 100 by default, any number with `limit`, and `nextCursor` reaches the rest |
| Document returned inline | 4MB, above which `contentBase64` is `null` rather than truncated |
| Time | 55 seconds per request. A signing is queued, so it is not bound by this |

Every request, a refused one included, is recorded under **Logs** in the global settings: what was
called, the outcome and the account it acted as. Never the token.

## Endpoints

Every reply is JSON, errors included. `pageId` is required on every endpoint that takes a
`reference`, so a reference from one page cannot address a signing on another.

| Endpoint | Does | Needs |
|---|---|---|
| `POST /v1/signings` | Queues a signing. `202` and a job | Edit |
| `GET /v1/pages/{pageId}/signing-jobs/{jobId}` | How a queued signing went. Kept for 7 days | The account that queued it |
| `GET /v1/pages/{pageId}/status` | Where a page stands, and its signings, newest first | View |
| `GET /v1/signings/{reference}?pageId=` | One signing, with counts rather than its people | View |
| `GET /v1/signings/{reference}/signers?pageId=` | The people on a signing, a page at a time | View |
| `GET /v1/signings` | Signings on a page with `?pageId=`, or every signing the account started | View |
| `DELETE /v1/signings/{reference}?pageId=` | Cancels a running signing, or deletes a finished one | See above |
| `POST /v1/pages/{pageId}/pdf` | Exports the page as PDF and attaches it | Edit |
| `GET /v1/pages/{pageId}/pdf/{attachmentId}` | A document SignPlus attached to the page | View |
| `GET /v1/signings/{reference}/document?pageId=` | The signed document, once everyone has signed | View |

### POST /v1/signings

| Field | Required | Notes |
|---|---|---|
| `pageId` | **yes** | The page to sign |
| `signers` | **yes** | At least one, below |
| `name` | no | What the document is called. The page title when left out, numbered when that name is already attached |
| `type` | no | `pdf`, `asice`, `bdoc`, `edoc` or `adoc`. The space decides which are allowed |
| `message` | no | Sent to signers. The space may require it or forbid it |
| `deadline` | no | ISO 8601 date and time with an offset, in the future |
| `hardDeadline` | no | Refuse signatures after the deadline. The space may overrule |
| `lockPage` | no | Make the page read-only while it is signed. The space may overrule |
| `restrictViewers` | no | Restrict viewing to the participants. The space may overrule |
| `includeAttachments` | no | Append the page's attachments |

A signer is a person or a group, with `role` `signer` (the default) or `viewer`:

```json
{ "email": "anna@example.com", "name": "Anna Tamm", "role": "signer" }
{ "group": "finance-team", "role": "signer" }
{ "groupId": "0f6a1b2c-...", "role": "viewer" }
```

- **A person is named by email address.** When the address belongs to a Confluence user who can
  view the page, they are added as that user. Anybody else signs as an external signer.
  Confluence cannot be searched by address, so the match is made by `name` and confirmed against
  the account's own address.
- **A group is named by its name or its id**, and every member is invited with the group's role.
  A member Confluence gives no address for cannot be invited, and the job's `message` says how
  many. Unlike the panel on the page, a service account is not limited to groups it belongs to:
  its Confluence permissions decide.
- **`spaceId` is not a field.** SignPlus asks Confluence which space the page is in, so a caller
  cannot choose which space's rules apply.

### GET /v1/pages/\{pageId\}/status

| `state` | Means |
|---|---|
| `ready-to-sign` | Nothing has been started on this page |
| `pending-unmodified` | A signing is out and the page has not changed |
| `pending-modified` | A signing is out and the page has changed since it was sent |
| `signed-valid` | Signed, and the page is exactly what was signed |
| `signed-modified` | Signed, and the page has been edited since |
| `declined` | A signer refused |
| `unknown` | The stored status is not one of the above |

The newest signing decides the state, by the same hash the byline uses.

### GET /v1/signings/\{reference\}/signers

| Query | Notes |
|---|---|
| `limit` | People per reply, 100 by default |
| `cursor` | The `nextCursor` of the previous reply |
| `status` | `pending`, `signed` or `declined` |
| `email`, `accountId` | Repeat either to ask about specific people. `notFound` lists anyone never asked |

A reply can come back shorter than `limit` when reading more would outrun the request's time.
Only an absent `nextCursor` means you have everybody. **This is the one place SignPlus returns a
signer's email address**, to an account that can view the page, because chasing the people who
have not signed needs it. The panel on the page never sends one to a browser.

### DELETE /v1/signings/\{reference\}

A running signing is cancelled: removed from Dokobit for every signer, with its page restrictions
lifted. A finished one is deleted: its record goes, and the signed file stays attached to the page.
The same permissions apply as in the panel: see
[cancelling or deleting a signing](../after-signing/#cancelling-or-deleting-a-signing). A `502`
means Dokobit could not be reached or the page restrictions could not be put back; nothing was
removed, and sending it again is safe.

### The PDF endpoints

`POST /v1/pages/{pageId}/pdf` takes `name`, which the `{documentName}` placeholder prints, and
`includeAttachments`. Add `?inline=true` to have the bytes back as base64 beside the attachment's
full Confluence address. Without Confluence credentials, keep the `attachmentId` and fetch the file
with `GET /v1/pages/{pageId}/pdf/{attachmentId}`.

`GET /v1/signings/{reference}/document` answers `409` with the signing's state until every signer
has signed and Dokobit has handed the file over.

## Errors

| Status | Means |
|---|---|
| `400` | Something is missing, or the space's rules refuse it. `error` says which |
| `401` | The token is not valid here. All reasons answer the same way |
| `402` | The site has no active SignPlus licence |
| `403` | The service account cannot reach that page, or may not do that to it |
| `404` | No such page, signing, job, document or path |
| `409` | The signing has no signed document yet |
| `413` | The body is over 2MB |
| `429` | Over 60 requests in a minute for this account |
| `500` | Something failed inside SignPlus. The detail is in the app's logs, never in the reply |
| `502` | Dokobit refused, or could not be reached |
| `503` | Signing is not configured on this site, for example no Dokobit token |

```json
{ "error": "At least one signer is required in \"signers\"." }
```

## What the API never returns

- **The Dokobit document token**, which downloads the signed file and is what a signer signs with.
  The `reference` is its SHA-256 and cannot be reversed.
- **A signer's Dokobit token**, for the same reason.
- **A stored copy of the document.** None is stored: a document comes back only from the PDF
  endpoints, which read it from the page.

## Things that will catch you out

- **The address is per installation.** A script that works on your sandbox has the wrong address
  for production.
- **The service account needs Confluence permissions, not only a credential.** A new one can
  authenticate and then be refused `403` by every page.
- **A signing is queued.** The `POST` answers before the signing exists. Follow the job, or the
  completion webhook, for the reference.
- **The space's settings win.** A refusal that mentions a format or a message points at the
  space's SignPlus settings.
- **Test mode is watermarked.** In **Test** signing mode, documents can only be signed with
  Dokobit's test identities and have no legal effect.
- **Tokens expire after an hour.** A script that caches one forever starts answering `401` exactly
  once, in production.

When you ask for help with a signing, quote its `reference`: it identifies the signing and reveals
nothing about its contents.

## Related

- [Automation and webhooks](../automation/)
- [Sign a page](../sign-a-page/)
- [Privacy and data handling](../privacy/)

---

A problem or a question? Write to [support@oktul.com](mailto:support@oktul.com) or open a request in the [Help Center](https://oktul.atlassian.net/servicedesk/customer/portals). Both reach the same service desk, so either way the request gets a reference and an SLA measuring the response.
