agrmnt API

Everything you can do in the app, you can do over HTTP: upload documents, add recipients, place fields, send, track, download sealed PDFs, and get webhooks. Base URL: https://agrmnt.io/api/v1

Quick start

Create an API key under Settings → API & webhooks, then send a PDF for signature in a single request. Put text such as {{sig}} in your PDF and agrmnt places the field on top of it.

curl https://agrmnt.io/api/v1/envelopes \
  -H "Authorization: Bearer $AGRMNT_API_KEY" \
  -F file=@contract.pdf \
  -F 'payload={
    "title": "Consulting Agreement",
    "message": "Hi Ada, please sign at your convenience.",
    "recipients": [
      { "name": "Ada Lovelace", "email": "ada@example.com" },
      { "name": "Charles Babbage", "email": "charles@example.com", "role": "cc" }
    ],
    "fields": [
      { "recipient": 0, "type": "signature", "anchor": { "text": "{{sig}}" } },
      { "recipient": 0, "type": "date", "anchor": { "text": "{{date}}" } }
    ],
    "send": true
  }'

The same request with JSON and a base64 file, in Node:

import { readFile } from "node:fs/promises";

const res = await fetch("https://agrmnt.io/api/v1/envelopes", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.AGRMNT_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    title: "Consulting Agreement",
    files: [{ filename: "contract.pdf", base64: (await readFile("contract.pdf")).toString("base64") }],
    recipients: [{ name: "Ada Lovelace", email: "ada@example.com" }],
    fields: [{ recipient: 0, type: "signature", page: 1, x: 0.1, y: 0.8, width: 0.3, height: 0.06 }],
    send: true,
  }),
});
const envelope = await res.json(); // { id: "env_…", status: "sent", recipients: [...], ... }

Authentication

Send your API key as a bearer token: Authorization: Bearer agr_…. Keys belong to a team, act as the admin who created them, and can be revoked at any time. Check a key with GET /me. Keep keys server-side — never ship them in a browser or mobile app.

Errors

Errors use conventional HTTP status codes and a consistent body:

{ "error": { "code": "validation_error", "message": "recipients.0.email: Invalid email" } }
  • 400 bad request · 401 invalid API key · 403 forbidden · 404 not found
  • 402 plan limit reached (plan_limit), e.g. monthly documents on the Free plan
  • 409 the action isn't allowed in the envelope's current state (e.g. editing a sent envelope)
  • 422 validation error · 429 rate limited · 5xx retry with backoff

Envelopes

An envelope is a document (one or more files merged into a single PDF) plus its recipients and fields. Lifecycle: draft → sent → completed, or declined / voided / expired.

Recipients

  • signer (default) fills in fields and signs. Needs at least one signature field.
  • approver reviews and approves. viewer must view and acknowledge. cc receives the completed PDF.
  • Set "sequential": true and routingOrder to sign in order; recipients sharing an order sign in parallel.
  • Identity verification: before seeing the document, every signer, approver and viewer confirms a one-time code sent to their email. Instances that enable SMS verification also require a mobile phone (international format, e.g. +14155550123) for those recipients. Results appear as emailVerifiedAt / phoneVerifiedAt on each recipient and in the audit trail.
  • accessCode requires the recipient to enter a code you share separately. embedded: true suppresses emails (see embedded signing).

Creating

POST /envelopes accepts multipart/form-data (one or more file parts, plus a JSON payload string) or application/json with files[].base64. Supported uploads: PDF, PNG, JPEG (up to 25 MB). Pass "send": true to send immediately, or leave it as a draft and call POST /envelopes/:id/send later. Attach your own identifiers with metadata (string key/values) — they come back in every response and webhook.

Updating drafts

PATCH /envelopes/:id updates settings. If you include recipients or fields, that list replaces the existing one. Keep a recipient's id to preserve it. The response includes recipientIds in the order you sent them.

Fields & placement

Field types: signature, initials, date, name, email, company, title, text, number, checkbox, dropdown.

  • date and email are filled automatically when the recipient signs; name is prefilled.
  • dropdown requires options. text/number accept a prefilled value. checkbox is optional by default.
  • Assign fields with recipientId, or recipient as an index into your recipients array, an email, or a template roleName.

Coordinates

By default x, y, width and height are fractions of the page (0–1) with the origin at the top-left, so they work at any page size. Use "units": "pt" for PDF points (1/72 inch). Width and height are optional and default to sensible sizes per type. Page sizes are returned in envelope.pages.

Anchor text

Instead of coordinates, give an anchor. agrmnt finds every occurrence of the text in the PDF and places the field with its bottom-left at the anchor's baseline. Tip: render anchors in white text so they're invisible.

{ "recipient": "ada@example.com", "type": "initials",
  "anchor": { "text": "[initial-here]", "offsetX": 0, "offsetY": 0, "occurrence": "all" } }

Templates

Templates store a document, recipient roles and field placement. Create them in the app or with POST /templates (same body as envelopes, recipients need a roleName). Then create envelopes from them, matching people to roles:

curl https://agrmnt.io/api/v1/envelopes -H "Authorization: Bearer $AGRMNT_API_KEY" -H "Content-Type: application/json" -d '{
  "templateId": "tpl_…",
  "recipients": [
    { "roleName": "Client", "name": "Ada Lovelace", "email": "ada@example.com" },
    { "roleName": "Company", "name": "You", "email": "you@yourco.com" }
  ],
  "metadata": { "crmDealId": "8842" },
  "send": true
}'

Embedded signing

Mark recipients "embedded": true to stop agrmnt emailing them, then request a URL when they're in your app. Show it in an iframe or redirect to it. With redirectUrl set, the signer is sent back with ?event=signing_complete.

curl -X POST https://agrmnt.io/api/v1/envelopes/env_…/recipients/rcp_…/signing-url -H "Authorization: Bearer $AGRMNT_API_KEY"
# { "url": "https://agrmnt.io/sign/…?embed=1", "status": "sent" }

The embedded page posts messages to the parent window: { source: "agrmnt", event: "loaded" | "completed" | "declined" }. Restrict which sites may frame it with the EMBED_ALLOWED_ORIGINS setting.

Documents & audit trail

Download PDFs from GET /envelopes/:id/documents/original, /completed (signed document with the certificate of completion appended, digitally sealed) and /certificate. Add ?download=1 for an attachment disposition. The x-content-sha256 header carries the file's fingerprint; anyone can check a completed file at /verify. GET /envelopes/:id/audit returns every event with timestamps, IP addresses and user agents.

Webhooks

Register endpoints in the app or with POST /webhooks. Subscribe to specific events or leave events empty for all:

  • envelope.sent
  • envelope.completed
  • envelope.declined
  • envelope.voided
  • envelope.expired
  • recipient.sent
  • recipient.viewed
  • recipient.completed
  • recipient.declined
POST /your/endpoint
agrmnt-event: envelope.completed
agrmnt-delivery: whd_…
agrmnt-signature: t=1760000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

{ "id": "whd_…", "event": "envelope.completed", "createdAt": "…", "data": { "envelope": { "id": "env_…", "status": "completed", … } } }

Verify the signature by computing an HMAC-SHA256 of `${t}.${rawBody}` with your endpoint secret and comparing it to v1. Reject timestamps older than a few minutes. Respond with any 2xx within 10 seconds; failures are retried with exponential backoff for about a day.

import crypto from "node:crypto";

export function verifyAgrmnt(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
  return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
import hashlib, hmac, time

def verify_agrmnt(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return abs(time.time() - int(parts["t"])) < 300 and hmac.compare_digest(expected, parts["v1"])

Endpoint reference

Machine-readable spec: /api/v1/openapi.json (OpenAPI 3.1 — import it into Postman, Insomnia or a code generator).

Envelopes

GET/envelopes
List. Query: status (comma separated), limit (≤100), page.
POST/envelopes
Create from files or templateId; optional send.
GET/envelopes/:id
Retrieve with recipients, fields and document links.
PATCH/envelopes/:id
Update a draft.
DELETE/envelopes/:id
Delete (void in-progress envelopes first).
POST/envelopes/:id/send
Send a draft.
POST/envelopes/:id/void
Void. Body: { "reason": "…" }
POST/envelopes/:id/remind
Re-email everyone currently waiting.
POST/envelopes/:id/duplicate
Copy. Body: { "as": "envelope" | "template" }
POST/envelopes/:id/documents
Append files to a draft (multipart).
GET/envelopes/:id/documents/:type
original · completed · certificate
GET/envelopes/:id/audit
Audit events.
PATCH/envelopes/:id/recipients/:rid
Correct name/email of a recipient who hasn't finished.
POST/envelopes/:id/recipients/:rid/resend
Resend their email.
POST/envelopes/:id/recipients/:rid/signing-url
Embedded signing URL.

Templates

GET/templates
POST/templates
GET/templates/:id
PATCH/templates/:id
DELETE/templates/:id

Webhooks

GET/webhooks
POST/webhooks
Body: { "url": "https://…", "events": [] } — response includes the signing secret.
PATCH/webhooks/:id
Change url, events or active.
DELETE/webhooks/:id
GET/webhooks/:id/deliveries
Last 50 deliveries.
POST/webhooks/:id/test
Send a webhook.test event.

Account

GET/me
The authenticated user.

Questions or missing something? Email admin@agrmnt.io and we'll help.