Outcome
Create a signature envelope as a DRAFT. Nothing is sent and no signer is contacted; the envelope stays editable until envelopes.send. Seed it from an active template, or supply the document, fields and recipients directly.
Before you start
- Verify the environment, authenticated identity and specifically approved organization context.
- The published application permission is workspace:write. Resolve and bind the exact canonical Sign workspace. A supplied id or organization membership alone grants no workspace access.
- Session callers require read_write or admin workspace access. API-key resource binding is verified by Auth; never treat a caller-supplied header as an independent grant.
- Approve the documented mutation and actual recipient/document targets before CLI --confirm or MCP confirm:true. A synthetic fixture authorizes no live agreement, notification or data deletion.
Discover the target
topolo whoami --json
topolo actions get app_topolo_sign.envelopes.create --detail --json
topolo resources topolo-sign --json
topolo workspaces list topolo-sign --json
- Set SIGN_WORKSPACE_ID to the exact granted workspace returned by discovery.
- Discover templateId through templates.list/get; confirm active status before creating an envelope from it.
- Discover documentId and documentRef from upload or authorized retained metadata; they are distinct identifiers.
Request
POST /api/envelopes
Inspect and test this action. Save the reviewed input as payload.json. Supply resource context separately.
Input fields
| Field | Required | Meaning | Constraints |
|---|---|---|---|
templateId | Optional | Exact template.id from templates.list/get; creating from a template requires active status. | {"minLength":1} |
title | Optional | Envelope display title; creation may derive it from the template. Draft updates preserve supported existing scalar values. | {"minLength":1} |
subject | Optional | Recipient message subject. Sending requires nonempty subject and message. | {} |
message | Optional | Recipient message. Editing a draft must also supply its full intended recipient list. | {} |
sourceApp | Optional | Optional originating application metadata; this does not grant permissions or discover a resource. | {"minLength":1} |
sourceRef | Optional | Optional origin resource reference metadata; this is not an authorization boundary. | {"minLength":1} |
documentTitle | Optional | Human document title metadata. | {"minLength":1} |
documentRef | Optional | Exact immutable document.documentRef returned by documents.upload. Never invent an object key or file path. | {"minLength":1} |
fields | Optional | Structured version-2 field objects with type, label, assignee and page placement. Geometry uses top-left page fractions. Send freezes the source and fields. | {"maxItems":200} |
fieldSchema | Optional | Legacy field-name input upgraded by the current source; use structured fields for explicit assignee, capacity and geometry. | {} |
recipients | Optional | Complete replacement list, at most 20. Omission during draft update clears recipients. Each has name, email, role, optional routingOrder and capacities; discover actual recipient identities and obtain authorization before notification. | {"maxItems":20} |
Read the complete input schema.
Effects
- Creates an envelope in DRAFT status.
- Sends nothing and contacts no signer — recipients are recorded, not notified.
- Copies the document, field schema and defaults from the template when templateId is supplied.
- Fields are structured (schema version 2): each has a type, label, required flag, an assignee {role, capacity} and page placement. The legacy fieldSchema name list is still accepted and upgraded.
- Recipients may carry a routingOrder (sequential signing) and company capacities (one person signing personally and for a company).
Interpret the response
Read $.envelope.id for the returned envelope identifier. An empty collection does not discover a target.
Expect 201 and read $.envelope.id. Confirm $.envelope.status is "draft" — creating an envelope never sends it. Call envelopes.get and check $.envelope.fields (fieldSchemaVersion 2) and the recipients before sending.
Inspect the success schema. CLI/MCP add their transport envelope; --detail requests the full CLI result. HTTP 204 contains no JSON payload.
Worked examples
Captured from the exported Sign Worker with the existing SQLite-backed D1 harness applying all core, envelope and audit migrations to one local database, in-memory R2 and synthetic Auth/workspace/archive/Notify transports. These examples prove local validation, persistence and artifact production. They do not prove separate deployed D1 topology, real recipient consent, provider delivery or a live signed agreement. All identities, signing links and documents are synthetic; replace identifiers through authorized discovery and never execute a fixture against a live organization. Binary download examples use the released SDK binary projection; tests Base64-decode and compare original bytes, lengths and server SHA-256 headers.
Supported request · Create a draft without notifying a recipient
Input · payload.json:
{
"templateId": "8a821f8b-b0d1-4808-a92e-8d3f16e4228f",
"title": "Fixture agreement",
"recipients": [
{
"role": "Signer",
"name": "Fixture Recipient",
"email": "recipient@example.test"
}
]
}
CLI:
topolo actions validate app_topolo_sign.envelopes.create --data @payload.json --json
topolo actions plan app_topolo_sign.envelopes.create --resource-type workspace --resource-id "$SIGN_WORKSPACE_ID" --data @payload.json --json
topolo actions call app_topolo_sign.envelopes.create --resource-type workspace --resource-id "$SIGN_WORKSPACE_ID" --data @payload.json --confirm --detail --json
MCP tool and arguments:
{
"tool": "topolo_call_action",
"arguments": {
"actionId": "app_topolo_sign.envelopes.create",
"input": {
"templateId": "8a821f8b-b0d1-4808-a92e-8d3f16e4228f",
"title": "Fixture agreement",
"recipients": [
{
"role": "Signer",
"name": "Fixture Recipient",
"email": "recipient@example.test"
}
]
},
"resourceType": "workspace",
"resourceId": "<exact granted id from discovery>",
"confirm": true,
"detail": true
}
}
Captured exported Worker response · HTTP 201:
{
"envelope": {
"id": "b0db0353-6d5a-4a9b-b3f4-85f439ec4953",
"templateId": "8a821f8b-b0d1-4808-a92e-8d3f16e4228f",
"title": "Fixture agreement",
"subject": "Review the fixture",
"message": "Synthetic local example only.",
"status": "draft",
"sourceApp": null,
"sourceRef": null,
"documentTitle": "Fixture agreement",
"documentRef": "sign-document:92c3741e-171c-439c-9419-597ffe485224",
"fieldSchemaVersion": 2,
"fields": [
{
"id": "fixture_signature",
"type": "signature",
"label": "Signature",
"required": true,
"assignee": {
"role": "Signer",
"capacity": "personal"
},
"placement": {
"page": 1,
"x": 0.1,
"y": 0.7,
"width": 0.3,
"height": 0.08
}
}
],
"fieldSchema": [
"fixture_signature"
],
"source": null,
"completedSha256": null,
"correlationId": "sgn_21282514-5ec2-4694-b413-1c113e14193b",
"completedAt": null,
"voidedAt": null,
"createdByEmail": "operator@example.test",
"createdAt": "2026-10-05T18:07:40.462Z",
"updatedAt": "2026-10-05T18:07:40.462Z",
"recipients": [
{
"id": "3d86fd6d-0500-49fd-bea4-4c7186a3cb87",
"envelopeId": "b0db0353-6d5a-4a9b-b3f4-85f439ec4953",
"role": "Signer",
"name": "Fixture Recipient",
"email": "recipient@example.test",
"status": "draft",
"routingOrder": 1,
"capacities": [
{
"key": "personal",
"kind": "personal"
}
],
"viewedAt": null,
"signedAt": null,
"declinedAt": null,
"typedName": null,
"evidenceSha256": null,
"linkIssuedAt": null,
"createdAt": "2026-10-05T18:07:40.462Z",
"updatedAt": "2026-10-05T18:07:40.462Z"
}
],
"auditEvents": [
{
"id": "bb368dd7-b119-44df-90cc-51647043472f",
"envelopeId": "b0db0353-6d5a-4a9b-b3f4-85f439ec4953",
"recipientId": null,
"eventType": "envelope.created",
"actorType": "user",
"actorEmail": "operator@example.test",
"metadata": {
"recipientCount": 1,
"fieldCount": 1,
"sourceApp": null,
"sourceRef": null
},
"createdAt": "2026-10-05T18:07:40.462Z"
}
]
}
}
HTTP 201. Expect 201 and read $.envelope.id. Confirm $.envelope.status is "draft" — creating an envelope never sends it. Call envelopes.get and check $.envelope.fields (fieldSchemaVersion 2) and the recipients before sending.
Supported request · Create a second untouched draft
Input · payload.json:
{
"title": "Disposable local draft"
}
CLI:
topolo actions validate app_topolo_sign.envelopes.create --data @payload.json --json
topolo actions plan app_topolo_sign.envelopes.create --resource-type workspace --resource-id "$SIGN_WORKSPACE_ID" --data @payload.json --json
topolo actions call app_topolo_sign.envelopes.create --resource-type workspace --resource-id "$SIGN_WORKSPACE_ID" --data @payload.json --confirm --detail --json
MCP tool and arguments:
{
"tool": "topolo_call_action",
"arguments": {
"actionId": "app_topolo_sign.envelopes.create",
"input": {
"title": "Disposable local draft"
},
"resourceType": "workspace",
"resourceId": "<exact granted id from discovery>",
"confirm": true,
"detail": true
}
}
Captured exported Worker response · HTTP 201:
{
"envelope": {
"id": "e4fc2bcd-481c-450a-bdbd-fbbbc93f6865",
"templateId": null,
"title": "Disposable local draft",
"subject": "Disposable local draft",
"message": "",
"status": "draft",
"sourceApp": null,
"sourceRef": null,
"documentTitle": "Disposable local draft",
"documentRef": null,
"fieldSchemaVersion": 2,
"fields": [],
"fieldSchema": [],
"source": null,
"completedSha256": null,
"correlationId": "sgn_8f7a0d29-cd77-4158-a40a-0f52804b6a89",
"completedAt": null,
"voidedAt": null,
"createdByEmail": "operator@example.test",
"createdAt": "2026-10-05T18:07:40.588Z",
"updatedAt": "2026-10-05T18:07:40.588Z",
"recipients": [],
"auditEvents": [
{
"id": "bf675b77-d67d-4964-a1e2-43d8e90a24fa",
"envelopeId": "e4fc2bcd-481c-450a-bdbd-fbbbc93f6865",
"recipientId": null,
"eventType": "envelope.created",
"actorType": "user",
"actorEmail": "operator@example.test",
"metadata": {
"recipientCount": 0,
"fieldCount": 0,
"sourceApp": null,
"sourceRef": null
},
"createdAt": "2026-10-05T18:07:40.588Z"
}
]
}
}
HTTP 201. Expect 201 and read $.envelope.id. Confirm $.envelope.status is "draft" — creating an envelope never sends it. Call envelopes.get and check $.envelope.fields (fieldSchemaVersion 2) and the recipients before sending.
Refused request · HTTP 401
Input · payload.json:
{
"templateId": "aee5622d-8cf4-41bc-873a-e2b0fe0f454f",
"title": "Fixture agreement",
"recipients": [
{
"role": "Signer",
"name": "Fixture Recipient",
"email": "recipient@example.test"
}
]
}
CLI:
topolo actions validate app_topolo_sign.envelopes.create --data @payload.json --json
topolo actions plan app_topolo_sign.envelopes.create --resource-type workspace --resource-id "$SIGN_WORKSPACE_ID" --data @payload.json --json
Captured exported Worker response · HTTP 401:
{
"error": "Token validation failed"
}
HTTP 401. Resolve the exact refusal before retrying. This request is not a successful workflow.
Refused request · HTTP 403
Input · payload.json:
{
"templateId": "aee5622d-8cf4-41bc-873a-e2b0fe0f454f",
"title": "Fixture agreement",
"recipients": [
{
"role": "Signer",
"name": "Fixture Recipient",
"email": "recipient@example.test"
}
]
}
CLI:
topolo actions validate app_topolo_sign.envelopes.create --data @payload.json --json
topolo actions plan app_topolo_sign.envelopes.create --resource-type workspace --resource-id "$SIGN_WORKSPACE_ID" --data @payload.json --json
Captured exported Worker response · HTTP 403:
{
"error": "This workspace requires read_write access",
"code": "workspace_access_insufficient"
}
HTTP 403. Resolve the exact refusal before retrying. This request is not a successful workflow.
Verify the result
- Expect 201 and read $.envelope.id.
- Confirm $.envelope.status is "draft" — creating an envelope never sends it.
- Call envelopes.get and check $.envelope.fields (fieldSchemaVersion 2) and the recipients before sending.
Errors and recovery
organization_required(HTTP 403): The token carries no organization scope. Re-authenticate against the organization that owns the workspace and retry.template_not_found(HTTP 404): No template in this workspace carries that id. Call templates.list and use an id from the response.template_not_active(HTTP 409): Only an active template can seed an envelope. Call templates.update with status "active" first, or supply the document, fields and recipients directly.invalid_fields(HTTP 400): fields must be an array of field objects (at most 200). Each needs an id (lowercase, digits, _ or -), a type and a label; detail names the offending field index or id.invalid_field_type(HTTP 400): type must be one of signature, initials, full_name, position, date, text, checkbox or company_capacity.invalid_field_placement(HTTP 400): placement is {page, x, y, width, height}: page is 1-based, the rest are fractions (0-1) of the displayed page from its top-left corner. Use null for the signature page.field_outside_page(HTTP 400): The box runs off the page: x + width and y + height must each be at most 1, and width and height must be positive.duplicate_field_id(HTTP 400): Every field id must be unique within the envelope; detail names the repeated id.invalid_routing_order(HTTP 400): routingOrder is a whole number from 1 to 20. Recipients sharing an order sign in parallel; higher orders wait for every lower one.capacity_company_required(HTTP 400): A company capacity needs companyName. Add {key, kind: "company", companyName, title?} to the recipient's capacities.too_many_recipients(HTTP 400): An envelope takes at most 20 recipients. Split the signing into separate envelopes.auth_error(HTTP 401): Reauthenticate in the same approved context before retrying.workspace_access_insufficient(HTTP 403): Discover the actual workspace grant and obtain the documented required access; do not retry with a guessed workspace or broader organization.
Some existing refusals return a human message as error rather than a stable code; preserve the actual response, status and request id. Platform validation, authentication and resource checks can refuse before the handler.
Recovery boundary
A draft has reached nobody, so it costs nothing to leave. Void it with envelopes.void if it should not be actionable at all.
Related actions
- Update envelope: Edit a draft envelope, replacing its recipients with the complete supplied list. Omitted recipients are cleared; sent or closed envelopes cannot be edited.
- Send envelope: Freeze the source and fields, revoke previous unsigned links, issue new signing links and queue recipient notifications in routing order. Notify acceptance is distinct from actual recipient delivery.
- Get Sign envelope: Get one signature envelope with its document, field schema, recipients and their individual signing status, and the delivery record for every party message ($.envelope.deliveries: signing requests and completed copies, with Notify dispatch outcome).
- Void envelope: Void a signature envelope so its outstanding signing links stop working and no further signature can be collected. A completed envelope cannot be voided; voiding is the only way to retract one already sent.
Evidence
Captured from the exported Sign Worker with the existing SQLite-backed D1 harness applying all core, envelope and audit migrations to one local database, in-memory R2 and synthetic Auth/workspace/archive/Notify transports. These examples prove local validation, persistence and artifact production. They do not prove separate deployed D1 topology, real recipient consent, provider delivery or a live signed agreement. All identities, signing links and documents are synthetic; replace identifiers through authorized discovery and never execute a fixture against a live organization. Binary download examples use the released SDK binary projection; tests Base64-decode and compare original bytes, lengths and server SHA-256 headers.