Send agreements from your own software
A small, predictable REST API and signed webhooks. Create a contract from a template when something happens in your system, hear back the moment it is signed, and keep the sealed file where your records live.
Overview
Base URL https://api.airesdocs.com/api/v1. JSON in, JSON out, over HTTPS. Authenticate with an API key created in Settings → API keys. For server-to-server use without keeping the key on the server, register a public key against it (RS256 or ES256) and exchange a short signed JWT at POST /api/v1/oauth/token (grant type urn:ietf:params:oauth:grant-type:jwt-bearer) for a one-hour token that acts as that key, with the same or fewer scopes. Most integrations need three things: create a contract from a template, send it, and receive the contract.completed webhook. Everything your integration does is recorded in the contract's audit trail like any other action, and the sealed file, certificate and public verification are the same as for contracts sent by hand.
Authentication and scopes
Send the key as Authorization: Bearer ad_live_…. Keys are shown once and stored hashed; each has a recognisable prefix, a last-used time and can be revoked instantly. Scopes limit what a key can do:
| Scope | Allows |
|---|---|
contracts:read | Get and list contracts, read field values and the audit trail, download the original and the sealed contract. |
contracts:write | Create, send, schedule, remind and void contracts; create embedded signing URLs. |
templates:read | List templates and their roles and fields. |
templates:write | Create, change, share and delete your own templates. |
webhooks:write | Register, list and remove webhook endpoints; read their delivery logs and retry deliveries. |
contracts:send | AI agents over MCP only: send contracts and reminders. |
contracts:void | AI agents over MCP only: void contracts. |
A key acts for the person who created it, and only they can see, change or revoke it. On Team, contracts a key creates belong to the team like any other. GET /api/v1/me tells you which account and scopes a key has.
Contracts
| Method | Path | What it does |
|---|---|---|
POST | /api/v1/contracts | Create a contract from a template (templateId, one recipient per role, prefill) or from one PDF (file, recipients, fields). Set send: true to send immediately or call send later. Send an Idempotency-Key header to make retries safe. |
POST | /api/v1/contracts/{id}/send | Send a draft now, or pass sendAt (ISO time, up to 90 days ahead) to schedule it. |
GET | /api/v1/contracts/{id} | Status, subject, dates, its files with their fingerprints, and every recipient with their state and viewed and signed times. Once completed, the final fingerprint and the public verification link. |
GET | /api/v1/contracts/{id}/fields | Every field with the value entered, who filled it and when (form data). Attachments are listed by name, size and fingerprint. |
PATCH | /api/v1/contracts/{id}/metadata | Set, change or remove your own key-value details on a contract (deal id, property reference, cost centre). Pass metadata on create too, and filter the list with metadata=key:value. |
GET | /api/v1/contracts/{id}/events | The audit trail in time order: sent, viewed, signed, reminded, voided and the rest, with who, when, IP address and device. |
GET | /api/v1/contracts/{id}/download | A short-lived link to the original (which=original, or one file of a pack with fileId) or, once completed, the sealed contract with its certificate (which=final). |
GET | /api/v1/contracts | List contracts, most recently changed first. Filters: status (comma list), templateId, label, folderId, q (title, recipient or field text), createdFrom and createdTo; paging with limit (up to 100) and offset. |
POST | /api/v1/contracts/{id}/remind | Remind everyone who still has something to do, or the recipientIds you name. Each person is reminded at most once every 12 hours; the response lists who was reminded and who was skipped and why. |
POST | /api/v1/contracts/{id}/void | Void a draft or sent contract with a reason. Outstanding links stop working and recipients who were invited are told. |
POST | /api/v1/contracts/{id}/recipients/{rid}/embed-url | A signing URL for one recipient to open inside your page (see embedded signing). Pass { "view": "focused" } for the minimal page: just the contract, the consent and one button. |
POST | /api/v1/contracts/{id}/recipients/{rid}/in-person | For a recipient with the role in_person: a one-off signing URL to open on the device in front of them, when it is their turn. The audit trail records that you hosted it. |
A create request from a template looks like the example above: a title, the templateId, one entry per role with name and email (and optionally its roleKey, otherwise roles are filled in order, plus phone, language, identity method and a private message), prefill values keyed by field key or label, and send: true. The contract is made exactly as Use template makes it in the app: every file in the template, every field with its rules, and the template's locked fields. Locked fields, and the fields they depend on, cannot be pre-filled, fields cannot be added to a template contract, and a draft whose locked field was changed is refused when it is sent. Send a file together with the templateId to use the template's fields on a document of your own: fields the template placed by text (anchors) are found again on the new file, the rest keep their page and position. For a contract made from a template, each recipient's role and signing order come from the template role they fill, not from role or order in the request. The response is the contract with its ID, status and recipients. Signature, initials, date-signed, name, email, attachment and stamp fields belong to the signer and cannot be pre-filled; GET /api/v1/templates marks every field that can.
Collecting a payment. Add payment to the create request: { "amountMinor": 95000, "currency": "GBP", "description": "Deposit", "recipient": 0 }, where recipient is the index of the signer who pays. mode can be fixed (the default), optional or adjustable (with minMinor and maxMinor, the signer chooses the amount). Payments need Pro or Team and your Stripe account connected in Settings, Payments; if anything is missing the request is refused and nothing is created.
Signing in person. Give a recipient the role in_person: they are not emailed. When it is their turn, call POST /api/v1/contracts/{id}/recipients/{rid}/in-person and open the URL it returns on the tablet or laptop in front of them.
Templates
| Method | Path | What it does |
|---|---|---|
GET | /api/v1/templates | List templates with their roles and every field: its key, the role that fills it and whether prefill can set it. |
GET | /api/v1/templates/{id} | One template with the same detail. |
POST | /api/v1/templates | Create a template from one PDF (base64) with its roles and fields, checked exactly as the app checks it. Needs templates:write. |
PUT | /api/v1/templates/{id} | Change the name, roles, fields or settings of one of your templates; only what you send changes. DELETE removes it. |
POST | /api/v1/templates/{id}/share | Share with your team, or only with chosen member groups (Team plan); { "shared": false } stops sharing. |
Most teams create and edit templates in the app, where the people who own the agreement can place fields and set routing. An integration can also create one from a PDF with POST /api/v1/templates: the roles, each field's role, type and position (fractions of the page), and settings such as the default subject and expiry. It is checked exactly as the app checks it, and plan limits apply. Each field has a stable key: pre-fill by that key (or by the field's label) when you create a contract.
Bulk send
Send one template to many people: POST /api/v1/bulk with the templateId and either rows (one per contract: the people by role key, optional phone and accessCode, and pre-fill fields) or a csv. You get back a preview with every row checked (names, email addresses, mobile numbers in international form, access codes of 4 to 64 characters, pre-fill values); nothing is sent until you call POST /api/v1/bulk/{id}/start. To do both in one call pass start: true: it sends only when every row is valid, and otherwise answers 409 with the problems. Access codes are stored only hashed and never returned. The bulk.completed webhook tells you when a batch has finished. Bulk send needs Pro or Team and a live key.
| Method | Path | What it does |
|---|---|---|
POST | /api/v1/bulk | Bulk send: a template and rows (or a CSV) → a preview with a check of every row; nothing is sent until you start it, or pass start: true to send when every row is valid. |
GET | /api/v1/bulk/{id} | A batch with its progress and each row. POST /api/v1/bulk/{id}/start sends it, /cancel stops what has not gone yet; GET /api/v1/bulk lists batches. |
Webhooks
Register an endpoint and choose events. Every delivery is a POST with a JSON body { id, type, created, data }, where data always carries the contractId and title, and an X-AiresDocs-Signature header (t=…,v1=…, an HMAC-SHA256 with the endpoint's secret over the time and the raw body). Verify before acting; respond with a 2xx quickly and do the work afterwards. Endpoints must be public HTTPS URLs; redirects are not followed. Each endpoint has its own secret. When you rotate it you choose how long the old one keeps working (up to 7 days); until then every delivery is signed with both, so your receiver keeps working while you switch, and you can stop the old one early.
| Event | When |
|---|---|
contract.sent | The contract was sent and the first recipients have been invited. |
contract.viewed | A recipient opened their link (recipient ID, name, email and role). |
contract.signed | A recipient signed. |
contract.completed | Everyone has finished. Includes the final fingerprint, whether it is sealed and the verification link; fetch the signed contract with the download endpoint. |
contract.declined | A recipient declined, with the reason. |
contract.voided | The contract was voided, with the reason. |
contract.expired | The contract reached its expiry date unsigned. |
contract.approved | An approver approved. |
contract.rejected | An approver rejected, with the reason. |
contract.delegated | A signer handed their part to someone else. |
contract.corrected | The sender corrected the contract after sending. |
contract.commented | A recipient or the sender added a comment. |
link.submitted | Someone signed up through one of your contract links. |
bulk.completed | A bulk send batch finished. |
payment.completed | A payment collected during signing succeeded. |
payment.refunded | A payment was refunded (in your Stripe dashboard, or automatically because it arrived after the contract closed). |
payment.disputed | The payer opened a dispute (chargeback) on a payment; respond in Stripe. |
| Method | Path | What it does |
|---|---|---|
POST | /api/v1/webhooks | Register an endpoint URL and the events to deliver. The signing secret is returned once. |
GET | /api/v1/webhooks | List your endpoints. DELETE /api/v1/webhooks/{id} removes one. |
GET | /api/v1/webhooks/{id}/deliveries | Delivery log for an endpoint: event, status, attempts, response code, next retry. |
POST | /api/v1/webhooks/{id}/deliveries/{deliveryId}/retry | Try a failed delivery again now. |
Failed deliveries are retried six times over about a day and a half, and every attempt is logged with your endpoint's response code. Each delivery carries an ID so redeliveries are easy to ignore.
Embedded signing
Add the origins of your site to the API key, create the contract with that key and send it, then ask for a signing URL for one recipient with POST /api/v1/contracts/{id}/recipients/{rid}/embed-url. Open it in an iframe on one of those origins. Creating the URL replaces the link that was emailed to that recipient, so your page now owns their session. The signing page posts messages only to your allowed origins: { type: "airesdocs:signing", event, contractId, recipientId, status }, where event is ready, viewed, signed, declined, completed or error. Check that the message comes from the AiresDocs app origin, and confirm the final state with the API or the webhooks. Identity methods, the seal and the certificate are identical to normal signing.
Only the origins on the key can frame the signing page, and only for links made with that key; every other page of the app refuses to be framed. Each allowed origin must be a plain https:// origin (scheme, host and optional port, no path), up to ten per key. A minimal page: <iframe src="{url}" title="Sign the contract" allow="clipboard-write" style="width:100%;height:90vh;border:0"></iframe>, then window.addEventListener("message", (e) => { if (e.origin !== "https://app.airesdocs.com") return; /* e.data.event */ }). Pass { "view": "focused" } when creating the URL for the minimal page. If your page cannot use an iframe, open the same URL in a pop-up window; the messages work there too.
Webhooks, not polling
Please do not poll each contract for its status. Register a webhook for contract.completed (and any other events you need) and act when it arrives; deliveries are signed, retried for about a day and a half, and logged, so you can retry any you missed from the delivery log. If you need a safety net, run one catch-up query at most every 15 minutes: GET /api/v1/contracts?changedSince=<time of your last run> returns only the contracts that changed since then. Stop following a contract once it is completed, declined, voided or expired; nothing changes after that. The API itself only enforces the per-key rate limit (below), but integrations that poll every contract will run into it.
AI agents (MCP)
AI assistants that speak the Model Context Protocol, such as Claude Code, or the Claude and OpenAI APIs, can connect to https://api.airesdocs.com/mcp with an API key as a Bearer token. A key with read scopes lets an agent list and look up contracts and templates; it can send, remind or void only if the key also has contracts:send or contracts:void. The agent sees only the tools its key allows, and everything it does appears in the contract's activity like any API call. Connectors that sign in with OAuth (claude.ai, the ChatGPT app) are not supported yet.
Limits and errors
- Requests are rate-limited per key (120 a minute unless you set another limit on the key, up to 600). Responses carry
X-RateLimit-LimitandX-RateLimit-Remaining; over the limit you get429withRetry-After. - Every request is validated against a schema; failures return
400with the field and the problem in plain English. Errors always look like{ "error": { "code", "message", "issues"? } }. - A key whose account is no longer on Pro or Team gets
402; a missing scope is403; someone else's contract is404; files over the size or page limits are413. - Creating a contract is idempotent when you send an
Idempotency-Keyheader (up to 255 characters): repeating the same request with the same key within 24 hours returns the first contract withIdempotent-Replayed: trueinstead of making and sending another, and the same key with a different body is refused with409.
Reference
The authoritative reference with request and response schemas is published with the API source as API.md; it is the document this page summarises, and the changelog records API changes. Until the public URL is live, email support@airesdocs.com and we will send it. Security questions, including responsible disclosure, are on the security page; data-protection terms for what your integration sends are in the DPA.
Questions
Which plan do I need?
API keys, webhooks and embedded signing are on Pro (£12 a month or £99 a year) and Team. Free accounts cannot create keys. A 30-day free trial of Pro, with no card needed, is enough to build and test an integration.
Is there a separate sandbox environment?
No. Build against your trial or paid account: send contracts to your own addresses, point webhooks at a test URL and use embedded signing from a test page on an https:// origin you have allowed on the key (for a page on your own machine, serve it over https, for example through a tunnel). If the account goes back to Free, its keys stop working (402) until it is on Pro or Team again.
How do I verify a webhook came from AiresDocs?
Each delivery carries an X-AiresDocs-Signature header of the form t=
Do webhooks retry?
Yes. A delivery that does not get a 2xx response is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 12 hours and 24 hours. The delivery log (in Settings and at GET /api/v1/webhooks/{id}/deliveries) shows every delivery, its attempts, the next retry time and your endpoint's last response code, and a failed delivery can be retried on demand.
Can I create a contract from a file rather than a template?
Yes. Send one PDF with the create request, either base64 in the JSON body or as a multipart file part, together with the recipients and the fields to place: either a page and position (fractions of the page) or an anchor, the text the field should sit next to, such as { "text": "Signed:", "occurrence": "all" }, so you never need coordinates. Most integrations use a template instead, because the fields, routing, identity methods and reminder defaults are then maintained in one place by the people who own the agreement.
Where is the full reference?
The authoritative reference, with request and response schemas, lives with the API source as API.md and is linked below. This page describes the contract in plain terms; if the two ever differ, the reference wins and we fix this page.
Build your integration on a 30-day Pro trial
No card needed. Create a key, send a contract to yourself from code, and watch the webhook arrive.