Sales
Agreements
Send documents for e-signature over the API, with an optional email code, ID check and payment. Then track each agreement until its sealed PDF is ready.
What an agreement is
An agreement is a document your recipients sign online. Every agreement has:
- a sealed PDF with a certificate page: an ID such as
VZC-8F2K4Q7M, a QR code to verify it, and SHA-256 fingerprints; - an audit trail that can't be changed: who opened, agreed, signed or declined, when, and from which IP address, place and device.
You can add these steps for every signer:
- a code we email them, which they enter before they can see or sign;
- an ID check: an ID document and a live selfie;
- a payment after signing, which can hold back their signed copy until it is paid.
You create an agreement from one of three sources:
- one of Vezmo's 57 templates, filled with your data (
templateKey); - one of your own templates, made in the dashboard (
templateId); - a PDF you upload, with signature and other fields placed on it (
documentId).
Agreements are always live. The emails go to real people, and the signatures are legally binding. A TEST key can create and read drafts only (see Test keys).
Contracts vs agreements
Contracts are the terms of a proposal: dates, payment notes and CC emails. Your client agrees to them by accepting the proposal in the client portal, and a proposal needs one before you can send it. Agreements collect a legally binding e-signature on any document, with an audit trail, a certificate and a sealed PDF. Use Agreements to get something signed. Use Contracts only for the terms of a proposal.
Scopes and keys
Each route needs a scope on your API key:
| Scope | Routes |
|---|---|
agreement.read | List templates, list and get agreements, read the audit trail, download the PDF. |
agreement.create | Upload a PDF, create a draft ("send": false), delete a draft. |
agreement.send | Create and send in one call (the default), send a draft, remind signers, approve or reject an ID check. |
agreement.void | Void a sent agreement. |
A create that sends needs agreement.create and agreement.send. Without agreement.send the answer is 403: "Sending needs the agreement.send scope. Pass "send": false to create a draft."
New keys get all four scopes. Older keys got them from the contract scopes they already had: contract.read added agreement.read, and contract.create added agreement.create, agreement.send and agreement.void.
Calls authenticate like the rest of the API. On your server, call POST /merchant/api-auth/login with your key and secret, and send the token it returns as Authorization: Bearer <accessToken>. See Create token.
Test keys
TEST keys can create and read drafts, but can't send, remind or void. Agreements have no test mode: sending emails real people a legally binding agreement. A TEST key can:
- create with
"send": false, and delete a draft; - list and get drafts, read their audit trail, and download their original PDF.
A list shows drafts only, and a status other than DRAFT gives an empty list. A sent agreement answers 404. Create-and-send, send, remind and void answer 403: "Test keys can create and read draft agreements only. Agreements have no test mode: sending emails real people a legally binding agreement, so use a live key to send, remind or void."
To try the full flow, use a live key and send the agreement to your own email address.
Lifecycle
Agreement status
| Status | Meaning |
|---|---|
DRAFT | Created with "send": false. Nothing was emailed. Send it or delete it. |
SENT | The invitations went out. No one has opened it yet. |
VIEWED | A signer opened it. |
PARTIALLY_SIGNED | At least one signer signed, and others still have to. |
COMPLETED | Everyone signed, and the sealed PDF is ready. |
DECLINED | A signer declined to sign. |
VOIDED | You voided it, in the dashboard or over the API, or Vezmo support did. |
EXPIRED | The deadline passed with someone still to sign. Remind brings it back to SENT. |
Each recipient has its own status: PENDING, OPENED, SIGNED or DECLINED.
Deadline and reminders
Set the deadline with options.expiresAt. Without one, it is 30 days after sending. The signing links work until the deadline. When it passes with someone still to sign, the agreement becomes EXPIRED and its payment link stops taking money.
In the 72 hours before the deadline, Vezmo reminds the signers who haven't signed, at most once a day each. A reminder carries a new link, and the earlier link stops working.
When everyone has signed
Right after the last signature, Vezmo seals the PDF and the agreement becomes COMPLETED. It can no longer be voided. Every signer is emailed the signed copy as an attachment, and you get it too. If the copy waits for payment, the signers get a payment link instead (see Payment).
ID check
With options.requireIdCheck, each signer uploads an ID document (a passport, or both sides of a national ID card) and takes a live selfie before signing. We check that:
- the face in the selfie is the face on the ID;
- the selfie is a live person, not a photo, a screen or a replayed recording. We look at the selfie video itself as well as the photos taken during it. The head movements we asked for (turn left or right, look up) must appear in the order we asked. A blink must appear when we asked for one. The face must stay the same person throughout, and the photos must show the person in the video;
- the name on the ID is the name you sent the agreement to.
Before the signer uploads anything, the signing page shows them our ID check consent: that you asked for the check, what we capture (photos of their ID and a short video selfie), what we do with it, that you see the result, the photos and the video, and how long we keep them. They tick a box to agree, or decline to sign. We record which version of the consent they agreed to, when, and from which IP address and device, and the agreement's audit trail shows "Agreed to the ID check". Nothing is captured without it.
When all of that holds, the check passes. When something can't be confirmed, the check waits for you: the signer can't sign until you approve it. Each recipient's identityCheck.status is one of:
| Status | Meaning |
|---|---|
NOT_REQUIRED | The ID check is off. |
PENDING | The check is on, and the signer hasn't sent an ID document yet. Also after you reject a check while the signer has tries left: review.outcome is then REJECTED, and the signer takes the check again. |
SUBMITTED | The ID document is in; the selfie is not. |
PASSED | The check passed, or you approved it (review.outcome is APPROVED). The signer can sign. |
NEEDS_REVIEW | Something couldn't be confirmed automatically, and reviewReasons says what. The signer can't sign until you approve it. They can also take the check again while they have tries left. See Review an ID check. |
FAILED | The last of 5 tries failed, or you rejected the check when no tries were left. The signer can't sign until you remind them, which resets the check. |
documentType is PASSPORT or NATIONAL_ID. attemptsLeft counts down from 5. decidedAt is when the check passed, needed review, or failed.
A selfie that is clearly not live uses up a try: a single still picture sent as the video, or a recording played on a loop. The signer is asked to take the check again. If we can't look at the selfie video on our side, the photos taken during it decide on their own, and the signer is never held up by our own fault.
reviewReasons lists why a NEEDS_REVIEW check needs you. It is empty in every other status. The codes are:
name_mismatch,name_unreadable: the name on the ID is not the recipient's name, or couldn't be read;borderline_face_match: the selfie only partly matches the ID photo;possible_spoof: the selfie may be a photo or a screen;frames_disagree,multiple_faces,few_usable_frames;pose_not_seen,liveness_not_shown,liveness_unverified: a head movement we asked for wasn't seen, or couldn't be checked;video_movement_not_seen,video_order_mismatch,video_blink_not_seen,video_multiple_faces,video_face_changed,video_frames_mismatch,video_unverified,looped_media: in the selfie video, a head movement we asked for wasn't seen or wasn't in the order asked; no blink was seen; another face appeared; the face changed; the selfie photos don't show the person in the video; the video couldn't be read or showed too little of the face; or parts of it repeat.
review is your last decision, { "outcome": "APPROVED" | "REJECTED", "at": "…" }, or null.
The API never returns ID images, the name read off the ID, face scores or liveness data. Look at those on the agreement in your dashboard.
We delete the signer's ID photos, selfie video and frames 3 years after their last step on the agreement (their latest ID upload, selfie, signature or decline, or your review of their check), or 3 years after the agreement is completed, declined, voided or expires if that is sooner; later only under a legal hold on your business. The result stays: identityCheck.status, documentType, decidedAt, the review, and the audit trail, which records "ID check photos and video deleted". If a signer withdraws their consent (they write to privacy@vezmo.com), we delete them sooner; an unfinished check on an open agreement then starts over.
What the signer sees:
- before the ID step, our ID check consent, which they agree to or decline to sign;
- after the ID and the selfie, a check that passed goes straight on to signing;
- one that needs review shows "Your ID is being reviewed", says we'll email them, and offers to take the check again while tries are left.
When you approve, we email the signer a new link to sign. When you reject, we email a new link to take the check again, or tell them to contact you if no tries are left. The new link replaces the one in earlier emails.
Routes
Paths are relative to the base URL https://api.vezmo.com/api/v1. Every JSON answer is the standard envelope: success, message, data, requestId and timestamp. Errors use the standard error body.
| Route | Scope |
|---|---|
GET /merchant/agreements/templates | agreement.read |
POST /merchant/agreements/documents | agreement.create |
POST /merchant/agreements | agreement.create + agreement.send |
GET /merchant/agreements | agreement.read |
GET /merchant/agreements/{id} | agreement.read |
GET /merchant/agreements/{id}/audit | agreement.read |
GET /merchant/agreements/{id}/document | agreement.read |
POST /merchant/agreements/{id}/send | agreement.send |
POST /merchant/agreements/{id}/remind | agreement.send |
POST /merchant/agreements/{id}/void | agreement.void |
POST /merchant/agreements/{id}/recipients/{recipientId}/id-check/approve | agreement.send |
POST /merchant/agreements/{id}/recipients/{recipientId}/id-check/reject | agreement.send |
DELETE /merchant/agreements/{id} | agreement.create |
List templates
GET /merchant/agreements/templates
Scope: agreement.read. No parameters.
system lists Vezmo's templates with their fields. custom lists your own.
curl https://api.vezmo.com/api/v1/merchant/agreements/templates \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"success": true,
"message": "Templates retrieved",
"data": {
"system": [
{
"key": "nda_v1",
"name": "Confidentiality Agreement (NDA)",
"description": "One-way confidentiality agreement protecting information disclosed by one party to another.",
"category": "Legal",
"parties": [
{ "order": 1, "key": "disclosing_party", "label": "Disclosing party" },
{ "order": 2, "key": "receiving_party", "label": "Receiving party" }
],
"fields": [
{ "key": "disclosing_party_name", "label": "Disclosing party name", "type": "text", "required": true },
{ "key": "receiving_party_name", "label": "Receiving party name", "type": "text", "required": true },
{ "key": "receiving_party_email", "label": "Receiving party email", "type": "email", "required": true },
{ "key": "purpose", "label": "Purpose of disclosure", "type": "multiline", "required": true },
{ "key": "confidential_info_definition", "label": "Definition of confidential information", "type": "multiline", "required": true },
{ "key": "effective_date", "label": "Effective date", "type": "date", "required": true },
{ "key": "duration_months", "label": "Duration (months)", "type": "number", "required": true },
{ "key": "jurisdiction", "label": "Governing jurisdiction", "type": "text", "required": true }
]
}
],
"custom": [
{
"id": "cm2tpl0001",
"name": "Lease",
"description": null,
"category": "Real estate",
"kind": "UPLOADED",
"roles": [ { "order": 1, "label": "Tenant" }, { "order": 2, "label": "Landlord" } ]
}
]
}
}- A field's
typeistext,multiline,email,date,number,currencyorselect. Aselectfield hasoptions. A number may haveminandmax. - A party's
orderis the recipient order to use inrecipients. - A party filled from your data takes its name and email from it. For example,
receiving_partycomes fromreceiving_party_nameandreceiving_party_email. A party with no such fields is your side, and it starts as the API key's user. - A custom template's
kindisUPLOADED(your own PDF with roles) orDERIVED(a Vezmo template with your default data).
Upload a PDF
POST /merchant/agreements/documents
Scope: agreement.create. Send multipart/form-data. Use the documentId it returns to create an agreement. Only your business can use the document.
| Field | Rules |
|---|---|
file | Required. A PDF of at most 10 MB and 100 pages. It is checked by its content, and it can't be password-protected or encrypted. |
curl -X POST https://api.vezmo.com/api/v1/merchant/agreements/documents \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-F "file=@consulting-agreement.pdf"{
"success": true,
"message": "Document uploaded",
"data": { "documentId": "5f0c0d6e-1b2a-4c3d-8e9f-0a1b2c3d4e5f", "pageCount": 3 }
}A file over 10 MB answers 413. Other problems answer 400:
- "Attach the PDF as the `file` field."
- "This file is not a PDF we can read. Upload a standard PDF."
- "This PDF is password-protected or encrypted, so it cannot be signed. Upload a copy without a password."
- "This PDF has 140 pages; an agreement can have at most 100."
Create an agreement
POST /merchant/agreements
Scope: agreement.create, plus agreement.send unless you pass "send": false. It creates the agreement and sends it, or keeps a draft.
| Field | Type | Rules |
|---|---|---|
title | string | Required, 1 to 255 characters. |
templateKey | string | A Vezmo template's key. Give exactly one of templateKey, templateId or documentId. |
data | object | With templateKey only. The template's fields, by key. Required fields, min, max and dates that can't be in the past are checked. |
templateId | string | One of your own templates (custom[].id). |
documentId | UUID | A PDF your business uploaded with Upload a PDF. |
recipients | array | 1 to 20 signers. Required with documentId and templateId. With templateKey it is optional: each recipient replaces the template party with the same order. |
recipients[].order | integer | 1 to 20, unique. The signing order. |
recipients[].name | string | Required, up to 120 characters. |
recipients[].email | string | A valid email, up to 254 characters, unique in the agreement. |
fields | array | With documentId only, up to 200. Where each signer signs or fills in. Every recipient needs at least one SIGNATURE field. |
fields[].type | string | SIGNATURE, INITIALS, DATE, TEXT or CHECKBOX. |
fields[].recipient | integer | The order of the recipient who fills it. |
fields[].page | integer | The page, from 1. It must exist in the PDF; this is checked when the agreement is sent. |
fields[].x, y, width, height | number | Fractions of the page from 0 to 1, measured from the top-left corner. |
fields[].required | boolean | Default true. |
options.requireEmailCode | boolean | Each signer enters a code we email them before they can see or sign. Default false. |
options.requireIdCheck | boolean | Each signer passes an ID check before signing. Default false. |
options.signingOrder | string | parallel (default): everyone signs at once. sequential: recipient 2 signs after recipient 1, and so on. |
options.branding | boolean | Your logo and colours on the signing page and emails. Left out, your business setting applies. |
options.expiresAt | string | The deadline, in ISO 8601 with a time zone. It must be in the future. Default: 30 days after sending. |
payment.amount | number | Required with payment. From 0.01 to the currency's maximum, at most 2 decimals. |
payment.currency | string | Required with payment. A supported currency, such as USD. |
payment.description | string | Up to 255 characters. Default: the agreement's title. |
payment.requiredForCopy | boolean | Hold back the signers' signed copy until the payment is made. |
send | boolean | Default true: create and send. false keeps a DRAFT that you send later with Send a draft. |
The emails name your business; there is no sender name to set. If any step of a create fails, nothing is left behind: no draft and no payment link.
Your own PDF, two signers in order, an email code and a payment:
curl -X POST https://api.vezmo.com/api/v1/merchant/agreements \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Consulting agreement",
"documentId": "5f0c0d6e-1b2a-4c3d-8e9f-0a1b2c3d4e5f",
"recipients": [
{ "order": 1, "name": "Jane Doe", "email": "jane@acme.com" },
{ "order": 2, "name": "Sam Lee", "email": "sam@yourco.com" }
],
"fields": [
{ "type": "SIGNATURE", "recipient": 1, "page": 3, "x": 0.12, "y": 0.80, "width": 0.30, "height": 0.05 },
{ "type": "DATE", "recipient": 1, "page": 3, "x": 0.50, "y": 0.80, "width": 0.15, "height": 0.04 },
{ "type": "SIGNATURE", "recipient": 2, "page": 3, "x": 0.12, "y": 0.88, "width": 0.30, "height": 0.05 }
],
"options": { "requireEmailCode": true, "signingOrder": "sequential", "expiresAt": "2026-11-30T23:59:59Z" },
"payment": { "amount": 250.00, "currency": "USD", "description": "Deposit", "requiredForCopy": true }
}'A Vezmo template. Recipient 1 replaces the disclosing party, which would otherwise be the API key's user. The receiving party comes from data.
{
"title": "NDA with Jane Doe",
"templateKey": "nda_v1",
"data": {
"disclosing_party_name": "Acme LLC",
"receiving_party_name": "Jane Doe",
"receiving_party_email": "jane@example.com",
"purpose": "Evaluating a partnership",
"confidential_info_definition": "All non-public business information shared between the parties.",
"effective_date": "2026-11-01",
"duration_months": 24,
"jurisdiction": "Delaware, USA"
},
"recipients": [ { "order": 1, "name": "Sam Lee", "email": "sam@acme.com" } ]
}The answer is 201 Created, with the message "Agreement sent", or "Agreement created" for a draft. data is the agreement:
{
"success": true,
"message": "Agreement sent",
"data": {
"id": "cm2agr0001",
"object": "agreement",
"title": "Consulting agreement",
"status": "SENT",
"source": "DOCUMENT",
"templateKey": null,
"proposalId": null,
"createdAt": "2026-10-18T10:00:00.000Z",
"updatedAt": "2026-10-18T10:00:02.000Z",
"expiresAt": "2026-11-30T23:59:59.000Z",
"completedAt": null,
"voidedAt": null,
"voidReason": null,
"certificateId": null,
"options": { "requireEmailCode": true, "requireIdCheck": false, "signingOrder": "sequential", "branding": null },
"recipients": [
{
"id": "cm2rec0001", "order": 1, "name": "Jane Doe", "email": "jane@acme.com",
"status": "PENDING", "viewedAt": null, "signedAt": null, "declinedAt": null, "declineReason": null,
"emailCodeVerified": false,
"identityCheck": { "status": "NOT_REQUIRED", "documentType": null, "attemptsLeft": null, "decidedAt": null, "reviewReasons": [], "review": null }
},
{
"id": "cm2rec0002", "order": 2, "name": "Sam Lee", "email": "sam@yourco.com",
"status": "PENDING", "viewedAt": null, "signedAt": null, "declinedAt": null, "declineReason": null,
"emailCodeVerified": false,
"identityCheck": { "status": "NOT_REQUIRED", "documentType": null, "attemptsLeft": null, "decidedAt": null, "reviewReasons": [], "review": null }
}
],
"fields": [
{ "id": "cm2fld0001", "recipientId": "cm2rec0001", "type": "SIGNATURE", "page": 3, "x": 0.12, "y": 0.8, "width": 0.3, "height": 0.05, "required": true, "value": null },
{ "id": "cm2fld0002", "recipientId": "cm2rec0001", "type": "DATE", "page": 3, "x": 0.5, "y": 0.8, "width": 0.15, "height": 0.04, "required": true, "value": null },
{ "id": "cm2fld0003", "recipientId": "cm2rec0002", "type": "SIGNATURE", "page": 3, "x": 0.12, "y": 0.88, "width": 0.3, "height": 0.05, "required": true, "value": null }
],
"payment": {
"amount": 250, "currency": "USD", "description": "Deposit", "status": "PENDING",
"requiredForCopy": true, "checkoutUrl": "https://user.vezmo.com/checkout/payments-links/AB12CD"
},
"signedDocumentAvailable": false
}
}sourceisTEMPLATE(a Vezmo template, or your template built on one),DOCUMENT(a PDF) orPROPOSAL(a proposal sent for signature from the dashboard).options.branding: nullmeans your business setting applies.fields[].valueis what the signer filled in, forTEXT,DATE(YYYY-MM-DD) andCHECKBOX("true"or empty). It is alwaysnullfor signatures and initials.signedDocumentAvailableistrueonce the agreement isCOMPLETED.- A response never has signing links, email codes or ID images. IP addresses and devices are only in the audit trail.
Create errors
| Code | When |
|---|---|
| 400 | "Give exactly one of templateKey, templateId or documentId." |
| 400 | data without templateKey, or fields without documentId. |
| 400 | "Add the recipients who sign." (documentId or templateId with no recipients) |
| 400 | "Two recipients have order 2." or "jane@acme.com is listed twice." |
| 400 | "Recipient 2 needs a SIGNATURE field." or "A field is for recipient 3, who is not in `recipients`." |
| 400 | "This agreement has no recipient 3: it has 2." (an order the template doesn't have) |
| 400 | "Recipient 2 needs a name and an email: add it in `recipients`." (a role of your own template was left empty) |
| 400 | "Missing required template fields: …" or "Invalid template fields: …" |
| 400 | "options.expiresAt must be in the future." |
| 400 | "A field is on page 5, but the document has 3 pages. Move it before sending." |
| 400 | "Invalid document reference." (a documentId your business didn't upload) |
| 400 | A payment amount you can't charge in that currency, or an unsupported currency. |
| 403 | Sending without agreement.send, or sending with a TEST key. |
| 404 | "Template not found" or "Unknown template: x" |
| 429 | The daily email cap (see Limits). |
List agreements
GET /merchant/agreements
Scope: agreement.read. Newest first. Each item is the agreement without fields.
| Query | Rules |
|---|---|
status | One agreement status. |
q | Searches the title and the signers' names and emails. Up to 100 characters. |
page | From 1. Default 1. |
limit | 1 to 100. Default 20. |
curl "https://api.vezmo.com/api/v1/merchant/agreements?status=VIEWED&limit=20" \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"success": true,
"message": "Agreements retrieved",
"data": {
"data": [
{ "id": "cm2agr0001", "object": "agreement", "title": "Consulting agreement", "status": "VIEWED", "…": "…" }
],
"total": 42,
"page": 1,
"limit": 20,
"totalPages": 3
}
}Get an agreement
GET /merchant/agreements/{id}
Scope: agreement.read. No body. Returns the agreement, with its signers' progress and the filled-in fields. It answers 404 "Agreement not found" when the agreement isn't your business's, or when a TEST key asks for a sent one.
curl https://api.vezmo.com/api/v1/merchant/agreements/cm2agr0001 \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"success": true,
"message": "Agreement retrieved",
"data": {
"id": "cm2agr0001",
"object": "agreement",
"title": "Consulting agreement",
"status": "COMPLETED",
"source": "DOCUMENT",
"templateKey": null,
"proposalId": null,
"createdAt": "2026-10-18T10:00:00.000Z",
"updatedAt": "2026-10-19T09:30:11.000Z",
"expiresAt": "2026-11-30T23:59:59.000Z",
"completedAt": "2026-10-19T09:30:10.000Z",
"voidedAt": null,
"voidReason": null,
"certificateId": "VZC-8F2K4Q7M",
"options": { "requireEmailCode": true, "requireIdCheck": false, "signingOrder": "sequential", "branding": null },
"recipients": [
{
"id": "cm2rec0001", "order": 1, "name": "Jane Doe", "email": "jane@acme.com",
"status": "SIGNED", "viewedAt": "2026-10-18T11:05:12.000Z", "signedAt": "2026-10-18T11:12:40.000Z",
"declinedAt": null, "declineReason": null, "emailCodeVerified": true,
"identityCheck": { "status": "NOT_REQUIRED", "documentType": null, "attemptsLeft": null, "decidedAt": null, "reviewReasons": [], "review": null }
},
{ "id": "cm2rec0002", "order": 2, "name": "Sam Lee", "email": "sam@yourco.com", "status": "SIGNED", "…": "…" }
],
"fields": [
{ "id": "cm2fld0001", "recipientId": "cm2rec0001", "type": "SIGNATURE", "page": 3, "x": 0.12, "y": 0.8, "width": 0.3, "height": 0.05, "required": true, "value": null },
{ "id": "cm2fld0002", "recipientId": "cm2rec0001", "type": "DATE", "page": 3, "x": 0.5, "y": 0.8, "width": 0.15, "height": 0.04, "required": true, "value": "2026-10-18" },
{ "id": "cm2fld0003", "recipientId": "cm2rec0002", "type": "SIGNATURE", "page": 3, "x": 0.12, "y": 0.88, "width": 0.3, "height": 0.05, "required": true, "value": null }
],
"payment": {
"amount": 250, "currency": "USD", "description": "Deposit", "status": "PAID",
"requiredForCopy": true, "checkoutUrl": "https://user.vezmo.com/checkout/payments-links/AB12CD"
},
"signedDocumentAvailable": true
}
}Audit trail
GET /merchant/agreements/{id}/audit
Scope: agreement.read. No body. The events, oldest first. These are the facts the certificate prints.
curl https://api.vezmo.com/api/v1/merchant/agreements/cm2agr0001/audit \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"success": true,
"message": "Audit trail retrieved",
"data": {
"certificateId": "VZC-8F2K4Q7M",
"events": [
{
"id": "cm2evt0001", "type": "REQUEST_SENT", "at": "2026-10-18T10:00:02.000Z",
"title": "Sent for signature", "detail": null,
"actor": { "kind": "sender", "name": "Sam Lee", "email": "sam@yourco.com" },
"ipAddress": null, "location": null, "device": null
},
{
"id": "cm2evt0002", "type": "RECIPIENT_SIGNED", "at": "2026-10-18T11:12:40.000Z",
"title": "Signed", "detail": "2 fields completed",
"actor": { "kind": "signer", "name": "Jane Doe", "email": "jane@acme.com" },
"ipAddress": "203.0.113.9", "location": "Dhaka, Bangladesh", "device": "Chrome on Android"
}
]
}
}actor.kindissigner,senderorsystem.ipAddress,locationanddeviceare set for people, andnullfor system events.titleanddetailare for people to read.detailisnullon ID-check events.
Download the PDF
GET /merchant/agreements/{id}/document
Scope: agreement.read. The answer is the PDF itself, from the API, as an attachment.
| Query | Rules |
|---|---|
version | signed (default): the sealed PDF with its certificate page, once the agreement is COMPLETED. original: the document as sent. |
You get the full signed copy here even while the signers' copy waits for payment. Before the agreement completes, version=signed answers 409: "The signed copy is ready once everyone has signed. Ask for version=original for the document as sent."
curl -o signed.pdf \
"https://api.vezmo.com/api/v1/merchant/agreements/cm2agr0001/document?version=signed" \
-H "Authorization: Bearer $ACCESS_TOKEN"HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="Consulting-agreement-signed.pdf"
Cache-Control: no-store
<the PDF bytes>Send a draft
POST /merchant/agreements/{id}/send
Scope: agreement.send. No body. Sends a DRAFT and answers 200 with the agreement. Any other status answers 409: "Only a draft can be sent; this agreement is SENT." If two sends arrive at once, one sends and the other gets 409.
curl -X POST https://api.vezmo.com/api/v1/merchant/agreements/cm2agr0002/send \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"success": true,
"message": "Agreement sent",
"data": { "id": "cm2agr0002", "object": "agreement", "status": "SENT", "…": "the agreement, as in Create" }
}Remind signers
POST /merchant/agreements/{id}/remind
Scope: agreement.send. No body. Emails a new link to every recipient who hasn't signed yet, and their earlier links stop working. reminded is how many were emailed.
- It works on
SENT,VIEWED,PARTIALLY_SIGNEDandEXPIREDagreements. Others answer409: "There is no one to remind: this agreement is COMPLETED." - An
EXPIREDagreement goes back toSENT, with a new deadline 30 days away. - A signer whose ID check failed can start it again.
- Once a minute per agreement at most. Sooner answers
400: "please wait 42s before resending".
curl -X POST https://api.vezmo.com/api/v1/merchant/agreements/cm2agr0001/remind \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"success": true,
"message": "Signers reminded",
"data": {
"reminded": 1,
"agreement": { "id": "cm2agr0001", "object": "agreement", "status": "PARTIALLY_SIGNED", "…": "…" }
}
}Void an agreement
POST /merchant/agreements/{id}/void
Scope: agreement.void. The signing links stop working, the payment link stops taking money, and signers who had opened or signed it are emailed.
| Field | Type | Rules |
|---|---|---|
reason | string | Optional, up to 500 characters. Kept as voidReason and in the audit trail. |
These answer 409:
- a draft: "A draft was never sent: delete it instead."
- an agreement that already ended: "This agreement is already COMPLETED."
- an agreement everyone has signed: "Everyone has signed and the signed copy is being made, so this agreement can no longer be voided."
curl -X POST https://api.vezmo.com/api/v1/merchant/agreements/cm2agr0001/void \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "reason": "Terms changed" }'{
"success": true,
"message": "Agreement voided",
"data": {
"id": "cm2agr0001",
"object": "agreement",
"status": "VOIDED",
"voidedAt": "2026-10-20T08:00:00.000Z",
"voidReason": "Terms changed",
"…": "…"
}
}Review an ID check
POST /merchant/agreements/{id}/recipients/{recipientId}/id-check/approvePOST /merchant/agreements/{id}/recipients/{recipientId}/id-check/reject
Scope: agreement.send. Decide a recipient's ID check whose identityCheck.status is NEEDS_REVIEW:
- Approve: the check becomes
PASSED, the signer can sign, and we email them a link to sign. - Reject: the signer takes the check again (
PENDING, with the tries they have left) and we email them a link to do it. With no tries left, it becomesFAILED, and you can remind them to start over.
We also email you, and show it in your dashboard, when a check needs review. You can review it there with the ID and selfie side by side.
Body (both optional):
| Field | Type | Rules |
|---|---|---|
attemptsLeft | integer, 0-5 | The identityCheck.attemptsLeft you reviewed. If the signer has taken the check again since, the call answers 409 instead of deciding on a check you haven't seen. |
note | string | Up to 500 characters, for your records. It is kept in the agreement's audit trail and is never shown to the signer. |
It answers 200 with the agreement, as Get an agreement returns it. The audit trail records "ID check approved by the sender" or "ID check rejected by the sender".
404"Agreement not found": not your agreement, or a test key (a test key sees drafts only).404"Recipient not found".409:- "This signer has not taken the ID check yet."
- "This ID check is not waiting for a review: it is PASSED."
- "This agreement is VOIDED, so there is nothing to review."
- "The signer has taken the ID check again since you opened it. Reload it and review the new one."
- "This ID check changed while you were reviewing it. Reload it."
403: the key lacksagreement.send.400: anattemptsLeftoutside 0-5, or a note over 500 characters.
curl -X POST https://api.vezmo.com/api/v1/merchant/agreements/cm2agr0001/recipients/cm2rec0001/id-check/approve \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "attemptsLeft": 3, "note": "Checked the passport on a video call" }'{
"success": true,
"message": "ID check approved",
"data": {
"id": "cm2agr0001",
"object": "agreement",
"status": "SENT",
"recipients": [
{
"id": "cm2rec0001", "order": 1, "name": "Jane Doe", "email": "jane@acme.com", "status": "PENDING", "…": "…",
"identityCheck": {
"status": "PASSED", "documentType": "PASSPORT", "attemptsLeft": 3, "decidedAt": "2026-10-18T12:40:00.000Z",
"reviewReasons": [], "review": { "outcome": "APPROVED", "at": "2026-10-18T12:40:00.000Z" }
}
}
],
"…": "the agreement, as in Get"
}
}Delete a draft
DELETE /merchant/agreements/{id}
Scope: agreement.create. No body. Drafts only, and the draft's payment link is cancelled. A sent agreement answers 409: "Only a draft can be deleted. Void a sent agreement instead."
curl -X DELETE https://api.vezmo.com/api/v1/merchant/agreements/cm2agr0002 \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"success": true,
"message": "Agreement deleted",
"data": { "id": "cm2agr0002", "deleted": true }
}Payment
- An agreement can have one payment, set with
paymentwhen you create it. The amount must be one you can charge, with the same minimums as a payment link. - Signers are asked to pay after signing. When the agreement completes, the checkout link (
payment.checkoutUrl) is emailed to them. - With
requiredForCopy: true, the signers get the signed copy only once it is paid, with the receipt added to the PDF. You always get the full copy, by email and from Download the PDF. payment.statusisPENDING,PAIDorCANCELLED. The link is cancelled when the agreement is voided, declined, expired or deleted. A paid link stays paid.agreement.paidtells you when it is paid.
Webhooks
Subscribe an endpoint to these events to follow your agreements:
| Event | Fires when |
|---|---|
agreement.sent | The agreement was sent. |
agreement.viewed | A recipient opened it for the first time. Once per recipient. |
agreement.signed | A recipient signed. Once per recipient; after the last one, agreement.completed follows. |
agreement.completed | Everyone signed, and the sealed PDF is ready. |
agreement.declined | A recipient declined to sign. |
agreement.voided | You voided it, in the dashboard or over the API, or Vezmo support did. |
agreement.expired | The deadline passed with someone still to sign. |
agreement.paid | Its payment was made, whether or not the copy was held for payment. |
agreement.identity_checked | A signer's ID check passed, needs your review, was approved or rejected by you (dashboard or API), or failed for the last time. recipient.identityCheck says which: status, plus review after your decision and reviewReasons while it needs review. |
Each event fires whatever caused it: the dashboard, the API, a signer, the deadline or Vezmo support. Each occurrence is its own delivery with its own envelope id, such as each signer's agreement.signed, or a second agreement.expired after a reminder revived the agreement. Dedupe on the envelope id.
{
"id": "evt_cm2ep001_agreement.signed_cm2agr0001_cm2evt0002",
"event": "agreement.signed",
"data": {
"id": "cm2agr0001",
"object": "agreement",
"status": "PARTIALLY_SIGNED",
"title": "Consulting agreement",
"occurredAt": "2026-10-18T11:12:40.000Z",
"livemode": true,
"recipient": {
"id": "cm2rec0001", "order": 1, "name": "Jane Doe", "email": "jane@acme.com",
"status": "SIGNED", "viewedAt": "2026-10-18T11:05:12.000Z", "signedAt": "2026-10-18T11:12:40.000Z",
"declinedAt": null, "declineReason": null, "emailCodeVerified": true,
"identityCheck": { "status": "NOT_REQUIRED", "documentType": null, "attemptsLeft": null, "decidedAt": null, "reviewReasons": [], "review": null }
},
"agreement": { "id": "cm2agr0001", "object": "agreement", "status": "PARTIALLY_SIGNED", "…": "the agreement, without fields" },
"companyId": "cm2co0001",
"merchantId": "VZ12345678"
}
}recipientis the recipient the event is about onviewed,signed,declinedandidentity_checked. It isnullon the others.agreement.paidalso carriespaymentId, and its envelopeidends with it.livemodeis alwaystrue: agreements have no test mode.- A failed delivery never changes the agreement.
Payloads are HMAC-signed. See the Webhooks guide for signature checks, retries and delivery logs.
Limits
| What | Limit |
|---|---|
| Upload, create, send, remind, void, delete, approve or reject an ID check | 2 a second, 10 per 10 seconds, 30 a minute, per IP address and per endpoint |
| Reads: templates, list, get, audit, document | 3 a second, 20 per 10 seconds, 60 a minute |
| Agreement emails | 500 invitations and reminders per business in 24 hours, unless support has set a different number for your business. Dashboard sends and automatic reminders count too. Over the limit, the answer is 429: "Your business has sent 498 agreement emails in the last 24 hours; its limit is 500. Try again later, or contact support to send more each day." |
| Uploaded PDF | At most 10 MB and 100 pages, not encrypted |
| Recipients | 1 to 20 per agreement |
| Fields | Up to 200 per agreement |
| Remind | Once a minute per agreement |
| Email codes | 5 an hour per signer |
See Limits for how rate limits work.
Not in v1
- Embedded signing URLs. Signing links change on every reminder, so the API doesn't return them.
- CC recipients.
- Sending a proposal for signature. It is in the dashboard only.
- A custom sender name. The emails always name your business.
- Editing a draft. Delete it and create it again.