Getting started
The API does one thing: it puts a document in front of somebody and proves who signed it. You raise a document with one call and learn the outcome from a webhook. Everything in between — the photo ID, the camera check, the emails — happens on our side.
| Gamma | https://api-gamma.thegentlemanagreement.com | Test environment. Nothing here is a real signature. |
|---|---|---|
| Signer client | https://gamma.thegentlemanagreement.com | Where a signer is sent. There is no API for signing on somebody's behalf. |
There is no sandbox mode on a production key. Use gamma, whose data is disposable and whose evidence bucket is not under a compliance lock.
Accounts and approval
Anyone can create an account at the console and start raising documents immediately. Nothing reaches the outside world until a person here has approved the account.
This is not a formality. An unapproved account that could email strangers would be a way to send mail from our domain, with our sending reputation, to any address it chose — a phishing kit with a signature ceremony attached. So the gate is on delivery and visibility, not on the API.
What a pending account can and cannot do
| While pending | Once approved |
|---|---|
| Create documents through the API | Same |
| Buy credits and subscribe | Same |
| No invitation email reaches a signer | Invitations are delivered |
| No signed or completed PDF is emailed to anyone | Both are delivered to every party |
| Signers cannot see or open the document at all | Everyone in the thread can |
| No API key is issued | Keys can be issued and revoked |
Held mail is kept, not dropped. Approval releases everything written while the account was waiting, in the order it was written, with fresh download links — a presigned URL minted a week earlier would arrive expired. The signer's first sight of the document is an ordinary invitation rather than one that never came.
A signer asking for a document from an unapproved owner is told it does not exist, in the same words an imaginary document gets. Saying "pending approval" would confirm to a stranger that this address was sent something, and by whom.
Signing up
POST /owners/signup | email, organisationName, password. The organisation name is what signers read: "X has asked you to sign". |
POST /owners/verify-email | email, code. Returns a console session. |
POST /owners/password/request | Emails a code to the address on the account. Answers 202 whether or not that address is a customer here. |
POST /owners/password/reset | Code plus a new password. Returns a console session. |
POST /owners/login | Email and password. An address that is not an owner fails exactly like a wrong password. |
POST /owners/me/proof | A photograph of a licence or a utility bill showing name and address. We read the text and keep that; the image is deleted after 30 days, like a signer's. |
GET /owners/me | Status, plan, credit balance, and whether proof has been sent. |
PUT /owners/me | name and webhookUrl. The URL must be https: the events carry hashes of signed documents. Setting the first URL issues webhookSecret, returned here and by GET /owners/me. |
POST /owners/me/webhook-secret | Rotates webhookSecret. Takes effect at once, including for retries already queued. |
POST /owners/me/email/request | Starts an address change. The code goes to the new address, because the current one is already proved by the session. |
POST /owners/me/email/confirm | Completes it. The password moves with the account; the old address stops finding it. |
POST /owners/me/password | Needs the current password, so a borrowed session cannot lock somebody out of their own account. |
GET /owners/me/documents | Everything you raised, each party, and who has signed. A document that stopped early carries closedAt and closedReason. |
POST /owners/me/documents/{documentId}/void | Withdraws a document from the console. Same rules as the API route. |
POST /owners/me/api-keys | Issues a key, once approved. Shown once — only a hash is stored. |
DELETE /owners/me/api-keys/{keyId} | Revokes it immediately. |
POST /owners/me/billing/checkout | Stripe checkout for credits or a plan. Allowed while pending. |
Authentication
Five kinds of caller, five credentials, and they are not interchangeable.
| Caller | Credential | Reaches |
|---|---|---|
| You, the document owner | x-api-key | Raising documents, templates, billing |
| A signer | Authorization: Bearer, minted after an identity check | Their own documents only |
| An account holder | Authorization: Bearer, console session | Their own account, keys, documents and billing |
| Somebody reading later | Authorization: Bearer, reader token from a password | Documents they already signed — never signing |
| An operator | Authorization: Bearer, operator token | The review queues, for accounts and for identity checks |
An API key is unique to one account, is shown once when issued, and is stored as a hash — a lost key is replaced, never recovered. Revoking one takes effect on the next request.
An API key identifies an owner and nothing else. It cannot sign, cannot read a signer's other documents, and cannot see an identity photograph — those are deleted on a schedule we do not let callers change.
Raising a document
POST /documents
Send one of three sources. Exactly one.
| Field | Type | Meaning |
|---|---|---|
title | string | What the signer sees in their list and in the subject line. |
signerEmails | string[] | Everyone who must sign. Each signs separately and each is verified separately. |
pdf | string, base64 | A finished PDF. Under 4.5 MB. |
markdown | string | Rendered by us. Simpler than shipping a PDF, and the source is kept with the evidence. |
templateId | string | An approved template, filled with values. |
requiresFreshCheck | boolean | Ignore any cached verification and make this signer prove identity again today. |
expiresAt | string, ISO 8601 | Optional deadline. Nobody may sign after it, and the document closes itself. In the future and at most 365 days away. |
curl -X POST https://api-gamma.thegentlemanagreement.com/documents \
-H "x-api-key: $TGA_API_KEY" \
-H "content-type: application/json" \
-d '{
"title": "Coaching agreement 2026",
"signerEmails": ["alex@example.com"],
"pdf": "JVBERi0xLjQKJ...",
"requiresFreshCheck": false
}'
{
"documentId": "doc_9f3c...",
"status": "open",
"billable": true,
"billingReason": "standard"
}
201, and the signer has an email. billable is decided at this
moment and does not change afterwards: a signer already verified this calendar month costs
nothing extra, whatever happens next.
Several signers
Each party signs the version the previous one produced, so the chain of sealed copies is the record of what each person actually read. There is no "send to all at once" that hides this.
Deadlines, withdrawing, declining
A document can stop without completing in three ways. Each one is final, each one ends it for every party who has not signed, and none of them touches a signature already given: that stays sealed, stays in the evidence export, and stays billed.
| Route | Who | Status afterwards |
|---|---|---|
POST /documents/{documentId}/void | You, with x-api-key | void |
POST /documents/{documentId}/decline | A signer the document is addressed to, with a signer or reader token | declined |
The deadline in expiresAt passes | Nobody — a scheduled pass notices | expired |
Both routes take an optional body, { "reason": "..." }, and answer with the
document's status, closedAt and closedReason. The reason
reaches you in the webhook; signers see only that the document closed.
Closing a document that is already complete or closed is a 409. So is signing
one — including after its deadline, even in the minutes before the scheduled pass has
caught up. A document belonging to another account is a 404, the same answer a
document that does not exist gets.
One refusal ends the document for everybody. The others are not asked to prove who they are for something that can no longer complete.
Templates
A template is markdown with {{placeholders}}, versioned and approved before use.
Raising from an unapproved version is refused — a document nobody signed off is not a
document you want in evidence.
GET /templates | List yours. |
POST /templates | Create one. |
PUT /templates/{templateId} | New draft version. |
POST /templates/{id}/versions/{n}/preview | Render without raising anything. |
POST /templates/{id}/versions/{n}/approve | Make a version usable. |
Webhooks
Webhooks are how you learn anything. They are queued and retried with backoff, so an endpoint that is down for an hour loses nothing.
| Event | When |
|---|---|
document.signed | A party signed. Carries both hashes. |
document.sent | An invitation went out to one party. For an account still under review, this happens on approval, not before. |
document.viewed | A party opened the document for the first time. Sent once per party, however often they reread it. |
document.completed | Every party has signed. |
document.voided | You withdrew it. Carries reason and signedBy. |
document.declined | A party refused. Carries partyEmail, reason and signedBy. |
document.expired | The deadline passed before everybody signed. Carries signedBy. |
verification.review | A signer's identity needs a human. Usually clears within a business day. |
Every delivery carries x-tga-signature, an HMAC-SHA256 of the raw body keyed with
your account's webhookSecret, which no other owner shares. Verify it
before acting, and compare in constant time. An unverified webhook is a stranger telling you a
contract was signed.
Evidence
GET /documents/{documentId}/evidence
What a dispute actually needs, and what a caller cannot alter:
presentedSha256— the bytes the signer read.sealedSha256— the bytes after their signature was written in.- The date they chose, and the true server time they confirmed it.
- The identity decision behind the signature, and which kind of document was shown.
Never the photographs and never the video. Those are deleted on a schedule, which is the point: what survives is proof that a check happened, not the biometrics that made it.
GET /ops/owners/{ownerId}/evidence exports everything
you own, including instructions for checking a hash without us. That export exists so leaving
is possible.
Billing
You pay per distinct signer per calendar month, never per document and never per seat. A signer is never charged.
| Plan | Price | Included |
|---|---|---|
| Prepaid credits | $1.00 per signer-month | Four documents for that signer |
| Small | $5 / month | 10 signer-months |
| Busier | $15 / month | 100 signer-months |
| Largest | $25 / month | 1000 signer-months, fair use above 150 new verifications |
A verification is cached for 36 months, so a returning signer costs us almost nothing and is
not billed again in the same month. GET /billing/balance
shows where you are.
Management
The management view at /ops.html is for the people running the platform. It
shows revenue by month, signatures billed against total, where the money comes from, and
every account with what its proof of identity said.
POST /ops/login | Email and password, checked by Cognito. Answers with the next step, never with a token. |
POST /ops/login/code | The six-digit code from an authenticator app. A token is minted only here. |
POST /ops/login/enrol | First sign-in: registers the authenticator and turns the requirement on. |
POST /ops/login/password | Replaces the temporary password an administrator created the account with. |
GET /ops/reports/revenue | Twelve months of revenue and signature counts, read from the billing ledger. |
GET /ops/owners | Accounts waiting for review. |
GET /ops/owners/all | Every account, with what its proof of identity said. |
POST /ops/owners/{ownerId}/decision | Approve or reject. Approving releases every message held while the account waited. |
POST /ops/owners/{ownerId}/block | Block or unblock a live account. |
PUT /ops/brand | Which of the four logomarks the site wears. |
PUT /ops/site-text | The marketing copy on the front page. A patch: a field left out keeps its words, and a field emptied returns to the copy the site shipped with. |
Blocking is not deletion. It stops new mail, hides the account's documents from signers and revokes its API keys immediately. It does not touch anything already signed: a signature is evidence, and evidence does not stop being true because we fell out with the person who asked for it. Unblocking restores the account to approved; the revoked keys stay revoked, and reissuing one is a deliberate act.
How an operator signs in
A Cognito user pool with mandatory TOTP. Cognito checks the password, Cognito checks the code, and only when Cognito is satisfied does this API mint a token of its own — so nothing here stores or verifies an operator password.
The pool is admin-create-only. There is no sign-up, no self-service recovery and no invitation link: an operator exists because somebody with AWS credentials created them. A new account's first sign-in enrols an authenticator before it is allowed any further, because a pool that requires MFA and an account without it is not a way in.
There is no fallback to a password held by this platform. An earlier build had one, gated by an allowlist and a bootstrap secret, and it is gone rather than kept beside this: a fallback to the weaker thing is the weaker thing.
Languages
Every page is written in English and can be read in any language an operator has switched on. A visitor picks a flag at the top of any page; the choice is kept in their browser and, once they sign in, with their account, so it follows them to the next device.
GET /i18n/languages | The languages on offer, each with its own name, a flag country and a text direction. Public. |
GET /i18n/{lang} | Every translation for one language, keyed by the English it translates. Public, and only for a language that is switched on. |
GET /site-text?lang={lang} | The front page, terms and privacy notice in that language, with English for anything not yet translated. |
GET /preferences/language | The signed-in person's language, or null. Any owner, signer or operator token. |
PUT /preferences/language | language. Refused unless it is on offer. |
Translations are written by operators only, from the management console. They are plain
text and are drawn as plain text: anything containing < or >
is refused rather than published.
Errors and limits
Errors are {"error": "a sentence"}. The status carries the meaning.
| Status | Means |
|---|---|
400 | The request is wrong. Repeating it unchanged will not help. |
401 | No credential, or one we do not recognise. |
403 | A valid credential of the wrong kind. |
409 | Already done. Signing is idempotent on x-idempotency-key. |
429 | Rate limited. Identity calls cost us real money, so they are limited per email, per IP and per day. |
502 | Something we depend on failed. You did nothing wrong; retry. |
A request body is capped at about 4.5 MB, which is an API Gateway limit rather than a policy.
What the signer does
Nothing to install, and no account.
- Photograph a driver's licence or passport.
- A short camera check, matched against the photo on the ID.
- A six-digit code to their email, which is what ties the identity to the address you gave us.
- Read, type their name, confirm.
No camera on their computer? The page offers a QR code that carries the whole check to their phone and hands the thread back when it is done. There is no weaker path: if the identity check cannot run, no signature is collected.
After signing, a signer can set a password and come back to read what they signed. That password mints a reader token and nothing more: it opens documents already signed and is refused by the signing route. A password proves somebody knows a secret, which is exactly the evidence this product exists to replace — so signing always means an ID and a live face, every time.
A signer's own profile is at GET /auth/profile:
their address, when their verification expires, and whether they have set a password.
POST /auth/profile/password changes it — somebody
who has just proved an ID and a live face may set one outright, while somebody who arrived
with a password must know the current one.
A signer's address is not editable. Documents were addressed to it by whoever asked for the signature, and the signatures on them record it; moving them would rewrite what the evidence says. A new address is simply a new address — the identity check still counts for three years, so there is nothing to repeat.
Signers must be 18 or over. A parent or guardian signs for a minor, and the record says so.