Developers

Connect Inkwell to your other tools

Create documents, add signers, send them and find out when they are signed, all from your own software. The same thing you do on the website, as a few plain web requests.

Getting started

The API is available on the Pro and Business plans. Create a key in Inkwell under Developers. It starts with ink_live_ and is shown only once, so copy it somewhere safe.

Every request goes to the API address and carries your key:

Setup
export INKWELL_API="http://inkwell.idey.click/api"
export INKWELL_KEY="ink_live_..."

# Every request sends:
# Authorization: Bearer $INKWELL_KEY

A key can work with documents and templates. It cannot reach billing, account or sign-in endpoints. Keep it private, and revoke it from the Developers page if it ever leaks.

Send a document

Five steps: create it, add who signs, say where they sign, send, then check on it.

1. Create a document

Upload a PDF as a multipart form with file and title.

POST /documents
curl -X POST "$INKWELL_API/documents" \
  -H "Authorization: Bearer $INKWELL_KEY" \
  -F "[email protected]" \
  -F "title=Website design agreement"

2. Add a signer

Use the document id from step 1. phone is optional.

POST /documents/:id/signers
curl -X POST "$INKWELL_API/documents/$DOC_ID/signers" \
  -H "Authorization: Bearer $INKWELL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","name":"Amaka Obi"}'

3. Place the fields

Fields are placed with fractions of the page from 0 to 1, measured from the top-left corner. So xFrac: 0.5 is halfway across and yFrac: 0.82 is near the bottom. type is one of signature, initials, date, text or checkbox. required and label are optional. This call replaces the document's fields with the list you send.

PUT /documents/:id/fields
curl -X PUT "$INKWELL_API/documents/$DOC_ID/fields" \
  -H "Authorization: Bearer $INKWELL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"fields":[{
    "signerId":"'"$SIGNER_ID"'",
    "pageNum":1,
    "type":"signature",
    "xFrac":0.1, "yFrac":0.82, "wFrac":0.3, "hFrac":0.06,
    "required":true,
    "label":"Client signature"
  }]}'

4. Send it

Signers get an email with their link. The message is optional.

POST /documents/:id/send
curl -X POST "$INKWELL_API/documents/$DOC_ID/send" \
  -H "Authorization: Bearer $INKWELL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"message":"Hi Amaka, please sign when you have a minute."}'

5. Check on it, then download

GET /documents/:id
curl "$INKWELL_API/documents/$DOC_ID" \
  -H "Authorization: Bearer $INKWELL_KEY"
GET /documents/:id/download
curl -L "$INKWELL_API/documents/$DOC_ID/download" \
  -H "Authorization: Bearer $INKWELL_KEY" \
  -o signed.pdf

Rather than asking again and again, use webhooks and let Inkwell tell you.

Templates

Start from a ready-made document

Browse the starter documents. GET /gallery lists them and GET /gallery/:slug shows the details you can fill in. yourRole is which party you are (a number, starting at 0), and people are the other parties. With send: true every other party needs an email. Anything you leave blank stays in the document as a visible [Label].

POST /gallery/:slug/use
curl -X POST "$INKWELL_API/gallery/mutual-nda/use" \
  -H "Authorization: Bearer $INKWELL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "values": {"partyA":"Ada Okafor Ltd","partyB":"Bola Studios","date":"1 March 2026"},
    "yourRole": 0,
    "people": [{"role":1,"email":"[email protected]","name":"Bola Studios"}],
    "send": true
  }'

Send a saved template to many people

Each person gets their own copy, link and certificate.

POST /templates/:id/bulk
curl -X POST "$INKWELL_API/templates/$TEMPLATE_ID/bulk" \
  -H "Authorization: Bearer $INKWELL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"rows":[{"name":"Amaka Obi","email":"[email protected]"},{"name":"Tunde Bello","email":"[email protected]"}]}'

Webhooks

A webhook is Inkwell calling your address when something happens. Add up to 5 endpoints on the Developers page. Each has its own secret. Addresses must be https and publicly reachable.

Webhook events
EventWhen
document.sentA document was sent for signature.
document.viewedA signer opened it.
document.signedOne signer finished signing.
document.completedEveryone has signed. The signed file is ready.
document.declinedA signer said no.
document.voidedYou cancelled the request.
payment.receivedA payment asked for during signing came through.

What you receive

Inkwell sends a JSON POST. signer is included for events about a person. downloadUrl appears only on completed documents and works for 7 days.

Example payload
{
  "id": "evt_01J9...",
  "event": "document.completed",
  "createdAt": "2026-10-10T09:41:07.000Z",
  "data": {
    "document": {
      "id": "doc_...",
      "title": "Website design agreement",
      "status": "completed",
      "sentAt": "2026-10-08T14:02:11.000Z",
      "completedAt": "2026-10-10T09:41:06.000Z"
    },
    "signer": { "id": "sig_...", "name": "Amaka Obi", "email": "[email protected]", "status": "signed" },
    "downloadUrl": "https://..."
  }
}

Two headers come with it: Inkwell-Event (the event name) and Inkwell-Signature, which looks like t=1760000000,v1=9f2c….

Check it really came from Inkwell

Compute an HMAC-SHA256 of {t}.{rawBody} using your endpoint's secret and compare it with v1. Use the exact bytes you received, and reject anything where the time is more than 5 minutes from now.

Node.js
import crypto from 'node:crypto';

// Use the RAW request body (a Buffer or string), not re-serialised JSON.
export function verifyInkwell(rawBody, header, secret) {
  const parts = Object.fromEntries(String(header ?? '').split(',').map((p) => p.trim().split('=')));
  const t = Number(parts.t);
  const sig = parts.v1;
  if (!t || !sig) return false;
  if (Math.abs(Date.now() / 1000 - t) > 300) return false; // too old or too new
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  const a = Buffer.from(sig, 'hex');
  const b = Buffer.from(expected, 'hex');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Express example:
// app.post('/inkwell', express.raw({ type: 'application/json' }), (req, res) => {
//   if (!verifyInkwell(req.body.toString('utf8'), req.get('Inkwell-Signature'), process.env.INKWELL_WEBHOOK_SECRET)) return res.sendStatus(400);
//   const event = JSON.parse(req.body.toString('utf8'));
//   res.sendStatus(200);
// });

Replies and retries

Answer with any 2xx status to say you got it. If you do not, Inkwell tries again after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours, then stops. The same event can arrive more than once, so use id to ignore repeats. The Developers page shows recent deliveries and what your server answered.

Verify a signed file

Anyone can check whether a PDF is exactly what was signed. This call is public, so it needs no key. Send the file's SHA-256 fingerprint. Your file never leaves your machine. You can also do this on the verify page.

POST /public/verify
# Get the file's SHA-256 fingerprint, then ask Inkwell about it. No key needed.
HASH=$(sha256sum signed.pdf | cut -d' ' -f1)
curl -X POST "$INKWELL_API/public/verify" \
  -H "Content-Type: application/json" \
  -d '{"sha256":"'"$HASH"'"}'

Limits

Limits are generous but not unlimited. If you send too many requests too fast you will get HTTP 429. Wait a little and try again. Errors come back as JSON with a message you can show or log.

Questions or something missing? Tell us.