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" } }400bad request ·401invalid API key ·403forbidden ·404not found402plan limit reached (plan_limit), e.g. monthly documents on the Free plan409the action isn't allowed in the envelope's current state (e.g. editing a sent envelope)422validation error ·429rate limited ·5xxretry 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 onesignaturefield.approverreviews and approves.viewermust view and acknowledge.ccreceives the completed PDF.- Set
"sequential": trueandroutingOrderto 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 mobilephone(international format, e.g.+14155550123) for those recipients. Results appear asemailVerifiedAt/phoneVerifiedAton each recipient and in the audit trail. accessCoderequires the recipient to enter a code you share separately.embedded: truesuppresses 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.
dateandemailare filled automatically when the recipient signs;nameis prefilled.dropdownrequiresoptions.text/numberaccept a prefilledvalue.checkboxis optional by default.- Assign fields with
recipientId, orrecipientas an index into yourrecipientsarray, an email, or a templateroleName.
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.sentenvelope.completedenvelope.declinedenvelope.voidedenvelope.expiredrecipient.sentrecipient.viewedrecipient.completedrecipient.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
/envelopesstatus (comma separated), limit (≤100), page./envelopestemplateId; optional send./envelopes/:id/envelopes/:id/envelopes/:id/envelopes/:id/send/envelopes/:id/void{ "reason": "…" }/envelopes/:id/remind/envelopes/:id/duplicate{ "as": "envelope" | "template" }/envelopes/:id/documents/envelopes/:id/documents/:typeoriginal · completed · certificate/envelopes/:id/audit/envelopes/:id/recipients/:rid/envelopes/:id/recipients/:rid/resend/envelopes/:id/recipients/:rid/signing-urlTemplates
/templates/templates/templates/:id/templates/:id/templates/:idWebhooks
/webhooks/webhooks{ "url": "https://…", "events": [] } — response includes the signing secret./webhooks/:idurl, events or active./webhooks/:id/webhooks/:id/deliveries/webhooks/:id/testwebhook.test event.Account
/meQuestions or missing something? Email admin@agrmnt.io and we'll help.