# Sigil API (v1)

A JSON API over your account: **templates** (documents with their glyphs read), **pacts** (a document sent to people for signature), and your **domains**. Everything the dashboard does with a document can be done here, under the same plan limits. Using the API (and MCP) needs the **Pro or Business** plan; any account can create keys.

- **Base URL:** `https://api.signwithsigil.com`
- **This reference:** `GET https://api.signwithsigil.com/v1` returns this page as Markdown (no key needed). Read it on screen at `https://www.signwithsigil.com/api`; the full documentation, with examples in several languages, is at https://docs.signwithsigil.com.
- **Format:** every response is JSON (`Content-Type: application/json`), UTF-8, never cached. Times are ISO 8601 UTC (`2026-10-20T14:05:00Z`).
- **MCP:** the same operations are available to AI assistants through the Model Context Protocol at `https://api.signwithsigil.com/mcp`; see the end of this page.

## Authentication

Create a key on `https://www.signwithsigil.com/api` after logging in (any number, each with a name; revoke them there too). Send it in either header:

```
X-Api-Key: sgk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Authorization: Bearer sgk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

A key starts with `sgk_` followed by 40 letters and digits and is case-sensitive. It acts as the user who made it. `401` means the key is missing, wrong or revoked; `403` means the account's plan does not include API access (Pro and Business do).

## Errors

Every error is `{ "error": "message" }` with a status of `400` (bad input, the message says what), `401`, `403`, `404` (not yours or does not exist), or `409` (a plan limit, such as not enough documents left to send, or an action the pact's state does not allow).

## Glyphs

Fields are read from glyphs, short codes typed into the document: `\s1\` signature, `\i1\` initials, `\n1\` printed name, `\d1\` date signed, `\t1:Label\` text (`\t1:Label!\` optional), `\c1\` checkbox (`\c1!\` optional). The number is the signer's order. A document uploaded through the API is refused if it has no glyphs, or if a signer has none. (On the website, paid plans can also place fields by hand; those come back here like any other field, at whatever size they were drawn.)

## Me

`GET /v1/me` → `{ "email", "plan", "member_level", "documents": { "available" (null = unmetered), "unmetered", "monthly": { "granted", "used", "remaining", "period_start", "resets" } (null when unmetered), "packs", "signers_per_document" }, "domains": [ "sign.example.com" ] }`

## Documents

Sending a pact uses **one document for every 3 signers**, rounded up (1–3 signers: 1, 4–6: 2, …); CC recipients are free. It is charged once, when the pact is sent, and is not returned if the pact is later voided, declined or lapses; resending a link is free. Documents come from the month's allowance first (resets on the 1st, UTC; no rollover), then from document packs (bought on the Billing page; never expire). Sending without enough documents answers `409` and the pact stays a draft. A draft shows what sending it will cost in `documents_cost`; a sent pact shows what it cost in `documents_charged`.

## Templates

| Call | What |
|---|---|
| `GET /v1/templates` | `{ "data": [template…], "total" }` (no fields) |
| `GET /v1/templates/{uid}` | one template with `fields` |
| `POST /v1/templates` | multipart: `file` (PDF or .docx, ≤ 10 MB), optional `name`. Returns `201` with the template. |
| `DELETE /v1/templates/{uid}` | `204`. Pacts made from it keep their own copy. |

A template: `{ "uid", "name", "source_name", "source_type": "pdf"|"docx", "page_count", "signer_count", "field_count", "date_created", "date_updated", "fields": [ { "recipient", "type", "page", "x", "y", "width", "height", "label", "required" } ] }`. Positions are PDF points; `y` is the field's baseline (the bottom of its box) from the top of the page.

```
curl -H "Authorization: Bearer sgk_…" -F file=@agreement.pdf -F name="Consulting agreement" https://api.signwithsigil.com/v1/templates
```

## Pacts

| Call | What |
|---|---|
| `GET /v1/pacts?status=&limit=&offset=` | `{ "data": [pact…], "total", "limit", "offset" }`. `status`: `active` (draft + sent), `all` (default), or one of `draft, sent, sealed, declined, voided, lapsed`. `limit` 1–200 (default 50). |
| `GET /v1/pacts/{uid}` | one pact with `fields`, `events` and `links`. Add `?include_links=1` to get each waiting signer's private `signing_url`. |
| `POST /v1/pacts` | create (and optionally send). JSON body below, or multipart with a `file` part for a one-off document (then send `recipients` as a JSON string). Returns `201`. |
| `POST /v1/pacts/{uid}/send` | send a draft (uses its documents). Optional JSON `{ "lapse_days": 14 }`. `409` when there are not enough documents left. |
| `POST /v1/pacts/{uid}/resend` | a new link for the signer whose turn it is (the old one dies). |
| `POST /v1/pacts/{uid}/void` | withdraw a sent pact. Optional JSON `{ "reason": "…" }`. |
| `DELETE /v1/pacts/{uid}` | delete a draft (`204`). Sent pacts are voided, not deleted. |
| `GET /v1/pacts/{uid}/document.pdf` | the document as sent |
| `GET /v1/pacts/{uid}/signed.pdf` | the stamped document, once sealed |
| `GET /v1/pacts/{uid}/sigil.pdf` | the pact's sigil (its certificate of completion), once sealed |

Create body:

```json
{
  "template_uid": "7DXW69YJ",
  "title": "Consulting agreement with Acme",
  "message": "Please sign by Friday.",
  "lapse_days": 14,
  "domain_host": "sign.example.com",
  "recipients": [
    { "name": "Avery Lane", "email": "avery@example.com", "role": "signer" },
    { "name": "Sam Ortiz", "email": "sam@example.com", "role": "signer" },
    { "name": "Records", "email": "records@example.com", "role": "cc" }
  ],
  "send": true
}
```

Signers are numbered in the order given and must match the document's glyphs; CCs receive the finished copy. `domain_host` must be one of your verified domains; leave it out to send under the primary site. With `"send": true` the first signer is emailed in the same call; the response then carries `"warning"` if that email could not be delivered.

A pact: `{ "uid", "title", "status", "message", "page_count", "created_by": "web"|"api"|"mcp", "current_signer", "date_created", "date_sent", "date_lapses", "date_sealed", "date_updated", "documents_charged", "documents_cost", "sent_sha256", "signed_sha256", "recipients": [ { "order", "role", "name", "email", "status": "pending"|"viewed"|"signed"|"declined", "date_viewed", "date_signed", "typed_name", "decline_reason", "signing_url"? } ], "fields": […], "events": [ { "event", "at", "by", "to", "note" } ], "links": { "document", "signed", "sigil", "dashboard" } }`.

Statuses: `draft` → `sent` → `sealed`, or `declined`, `voided`, `lapsed` (still unsigned at `date_lapses`). Signers are invited one at a time in order; `current_signer` is the order of the one whose turn it is. Poll `GET /v1/pacts/{uid}` for progress; outbound webhooks are not available yet.

## Domains

`GET /v1/domains` → `{ "data": [ { "host", "status": "pending"|"verified"|"disabled", "brand_name", "date_verified", "date_checked" } ], "total" }`. Domains are added and verified on the dashboard.

## MCP

The MCP server is hosted here, so there is nothing to install: point your MCP client at the endpoint and give it an API key.

- **Endpoint:** `https://api.signwithsigil.com/mcp` (Streamable HTTP).
- **Authentication:** your API key in `Authorization: Bearer sgk_…` (or `X-Api-Key`). `401` and `403` mean the same as for the API.
- **Tools:** `get_account`, `list_templates`, `get_template`, `list_pacts`, `get_pact`, `get_signing_status`, `create_pact`, `send_pact`, `resend_pact`, `void_pact`. They answer with the same JSON as the API calls above.

Claude Code:

```
claude mcp add --transport http sigil https://api.signwithsigil.com/mcp --header "Authorization: Bearer sgk_…"
```

A client configured with JSON (Claude Code's `.mcp.json`, and clients that use the same format):

```json
{ "mcpServers": { "sigil": { "type": "http", "url": "https://api.signwithsigil.com/mcp", "headers": { "Authorization": "Bearer sgk_…" } } } }
```

A client that can only launch local (stdio) servers needs a local bridge that forwards to an HTTP MCP server and adds the header.
