Signing with an agent
An AI agent is not a legal person. It cannot form intent, cannot be a party, and cannot hold an ESIGN/UETA signature of its own. Every mark DocTransit applies is the registered principal’s stored appearance, under a scoped Signing Mandate. The agent is a channel. There is no silent sign_document tool.
This is product documentation, not legal advice. Mandate consent copy has not been reviewed by counsel.
The wire protocol for two agents closing a deal is DASP. This page is how signing works for a single principal and their key.
What a mandate is
A Signing Mandate is an open authorization, not a signature. The principal consents once (clickwrap + email OTP) that a specific API key may later apply their stored mark, inside a scope they set.
| Field | Meaning |
|---|---|
| Confirmation level | L1 — principal confirms each apply. L2 — in-policy auto-sign + email notify. |
| Mode | outbound (docs the principal sent), inbound (docs sent to them), or both. |
| Expiry | Optional. Expired mandates cannot apply a mark. |
| Daily L2 cap | 25 applies per mandate per UTC day. |
Revoke anytime in Developer → Signing Mandates. Revocation does not unwind completed documents.
documents:sign is not on a key by default. Grant attaches it. Revoking the last active mandate for that key removes it.
Prerequisites
- Business+ plan (API access).
- An API key (Developer → API Keys).
- A stored signature under Settings → My Signature. L1/L2 apply that image only — the model cannot upload a new scribble.
- A Signing Mandate for that key.
- A sent document where the principal is a signer, with at least one
signaturewidget.
Required widgets the stored appearance cannot fill (text input, checkbox, dropdown, and similar) block apply (unfilled_fields). Auto-filled types: signature, initials (stored image, or letters from the principal’s name), date, name, email.
Grant (human, in the app)
- Open Developer → Signing Mandates.
- Choose the API key, a name, L1 or L2, inbound/outbound/both, and optional expiry.
- Accept the clickwrap (
mandate-consent-v2). - Confirm the email OTP.
The grant is the intent event. It does not stamp any PDF.
GET /v1/me returns signing_mandate when this key has an active mandate, plus flags and allowed_confirmation_levels. GET /v1/mandates lists mandates granted to this key.
L1 — confirm each signature
Use L1 when a human should see the document before the mark is applied.
create / send document
↓
POST /documents/:id/sign-requests (MCP request_sign)
↓
principal opens confirm_url
↓
Confirm and apply my signature
↓
stored appearance stamped → PDF signed → document.signedcurl -s https://api.doctransit.com/v1/documents/$DOC_ID/sign-requests \
-H "Authorization: Bearer $DOCTRANSIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"reason":"NDA matches standing policy"}'The response includes confirm_url (https://app.doctransit.com/sign-request/{token}). DocTransit also emails that link. The request expires in 24 hours. Decline leaves the document unsigned.
A pending request already exists → 409 pending_exists. Call GET /documents/:id/sign-requests instead of creating another.
L1 works with an L1 or L2 mandate. An L2 key can still escalate to confirm.
L2 — in-policy auto-sign
Use L2 when the principal has already authorized applies inside the mandate scope and does not want a per-document click.
create / send document
↓
POST /documents/:id/sign (MCP sign_as_principal)
↓
mandate + flag + widgets checked
↓
stored appearance stamped → PDF signed → email notify → document.signedcurl -s https://api.doctransit.com/v1/documents/$DOC_ID/sign \
-H "Authorization: Bearer $DOCTRANSIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"reason":"in-policy NDA"}'An L1 mandate calling this endpoint gets 403 confirmation_required — use request_sign. If l2_signing is off, the endpoint returns 403 feature_disabled.
Production currently has l2_signing on (DOCTRANSIT_FF_L2_SIGNING=true). The code default is still off when NODE_ENV=production unless that env (or DOCTRANSIT_ALLOW_L2) is set. See feature flags.
L2 is not silent: mandate grant + disclosed certificate + email after every apply + 25/day cap.
What happens on apply
Both L1 confirm and L2 sign run the same settlement path:
- Mandate must be
activeand cover the document (principal is a signer; mode matches inbound/outbound). - Document must be
sentand not already signed by the principal. - Strict signing order, if enabled, must not be waiting on a prior signer.
- Widgets must be stampable; stored signature bytes must be a readable PNG/JPEG.
- SHA-256 of the current PDF is recorded as
pre_sign_hash. - The stored appearance is drawn onto the principal’s widgets (
pdf-lib). - The existing
signPdfpath digitally signs the file and writes the audit trail. - Webhooks:
document.signed(anddocument.completedif every signer is done). - L2 also emails the principal. L1 already confirmed in the app.
The completion certificate discloses the principal, Signer (via authorized agent), and Mandate : {id} (L1|L2). The agent is never named as a party.
GET /documents/:id works for documents the principal owns and inbound documents where they are a signer.
Feature flags
Deploy-time switches, not plan entitlements. Resolution per flag:
DOCTRANSIT_FF_<NAME>(e.g.DOCTRANSIT_FF_L2_SIGNING=true)- Legacy alias (
DOCTRANSIT_ALLOW_L2forl2_signing) DOCTRANSIT_FFJSON blob ({"l2_signing":true})- Registry default (
l2_signingis off in production unless overridden)
GET /v1/flags and GET /v1/me return the snapshot. The app calls Parse getfeatureflags. Off flags hide the corresponding UI and return 403 feature_disabled.
| Flag | Env | Default |
|---|---|---|
l1_signing | DOCTRANSIT_FF_L1_SIGNING | on |
l2_signing | DOCTRANSIT_FF_L2_SIGNING | on in non-production; off in production unless env is set |
agent_contacts | DOCTRANSIT_FF_AGENT_CONTACTS | on |
agent_offers | DOCTRANSIT_FF_AGENT_OFFERS | on (requires contacts) |
agent_editor | DOCTRANSIT_FF_AGENT_EDITOR | on |
Surfaces
| Surface | L1 | L2 |
|---|---|---|
| App | Developer → Signing Mandates; /sign-request/:token | Same grant UI when L2 is allowed |
| REST | POST /documents/:id/sign-requests | POST /documents/:id/sign |
| MCP 0.3+ | request_sign / list_sign_requests | sign_as_principal |
| CLI 0.3+ | doctransit docs request-sign | doctransit docs sign |
| SDK 0.3+ | client.requestSign(id, reason) | client.signAsPrincipal(id, reason) |
Published npm 0.2.x clients can create and send documents but do not include the mandate tools. Use the Agents API, or MCP/CLI/SDK 0.3+.
Errors
| Code | When |
|---|---|
feature_disabled | Flag off for this deployment |
mandate_required / mandate_inactive | No active mandate for this key |
confirmation_required | L1 key called POST /sign |
principal_not_signer | Principal is not a signer on the document |
mandate_scope | Mode is outbound-only or inbound-only and does not cover this doc |
missing_appearance | No readable signature under Settings → My Signature |
missing_signature_fields | No signature widget for the principal |
unfilled_fields | Required widgets the appearance cannot fill |
already_signed | Principal already signed |
pending_exists | An open L1 request already exists |
strict_order | Prior signer has not signed |
rate_limited | L2 daily cap (25) reached |
sign_failed | Digital signature step did not succeed |
What this will not do
- Treat API key possession as intent to sign.
- Let a model upload a new scribble as the principal’s signature.
- Hide the agent on the certificate.
- Let tenant A’s agent sign as tenant B.
- Unwind completed documents when a mandate is revoked.
- Enable L3 silent corporate auto-sign (
mandate-consent-v2does not grant it).