Outcome
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.
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.send --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.
- Resolve envelopeId through envelopes.list/get or retain the exact creation response.
Request
POST /api/envelopes/{envelopeId}/send
Inspect and test this action. Save the reviewed input as payload.json. Supply resource context separately.
Input fields
| Field | Required | Meaning | Constraints |
|---|---|---|---|
envelopeId | Required | Exact envelope.id returned by creation or envelopes.list/get in this workspace. | {"minLength":1} |
Read the complete input schema.
Effects
- Mints a signing token for every unsigned recipient and returns one signing link per recipient in $.signingLinks, with each recipient's routingOrder.
- Emails each signer their own link through Topolo Notify, as the organization's end-user mail (from "<Organization> org-slug@mail.topolo.app", reply-to the sender). Signers in a later routing order are emailed when their turn opens, not before.
- Sending again (resend) revokes every outstanding unsigned link — the old URLs answer 410 signing_token_revoked — issues new ones and emails them; signatures already given are untouched.
- Once every signer has signed, each signer and the sender are emailed the completed PDF and the audit certificate (attached when small enough, always with a link). Delivery state per party is in envelopes.get $.envelope.deliveries.
- Anyone holding a signing link can open that recipient's signing session; treat the returned links as credentials.
- Freezes the signing source: the ingested document version, its SHA-256 and page geometry, and the structured field schema. Every signature binds to that hash.
- Moves the envelope out of draft, so it stops being editable.
- Signing links expire after the workspace signing-token lifetime, 14 days unless configured otherwise.
- Notification outbox acceptance does not prove a recipient received or opened the message. Read envelope.deliveries and the linked Notify dispatch outcome.
Interpret the response
Read $.envelope.id for the returned envelope identifier. An empty collection does not discover a target.
Read $.signingLinks and confirm one link per unsigned recipient. Confirm $.envelope.source.sha256 is set: that is the document hash every signature binds to. Call envelopes.get and confirm each recipient status reads "sent". Do not call this action twice to "make sure" — a second send re-mints tokens and the links you already shared stop working.
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.
Refused request · HTTP 409
Input · payload.json:
{
"envelopeId": "b0db0353-6d5a-4a9b-b3f4-85f439ec4953"
}
CLI:
topolo actions validate app_topolo_sign.envelopes.send --data @payload.json --json
topolo actions plan app_topolo_sign.envelopes.send --resource-type workspace --resource-id "$SIGN_WORKSPACE_ID" --data @payload.json --json
Captured exported Worker response · HTTP 409:
{
"error": "recipients_required"
}
HTTP 409. Resolve the exact refusal before retrying. This request is not a successful workflow.
Supported request · Freeze source, issue synthetic links and obtain Notify acceptance
Input · payload.json:
{
"envelopeId": "b0db0353-6d5a-4a9b-b3f4-85f439ec4953"
}
CLI:
topolo actions validate app_topolo_sign.envelopes.send --data @payload.json --json
topolo actions plan app_topolo_sign.envelopes.send --resource-type workspace --resource-id "$SIGN_WORKSPACE_ID" --data @payload.json --json
topolo actions call app_topolo_sign.envelopes.send --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.send",
"input": {
"envelopeId": "b0db0353-6d5a-4a9b-b3f4-85f439ec4953"
},
"resourceType": "workspace",
"resourceId": "<exact granted id from discovery>",
"confirm": true,
"detail": true
}
}
Captured exported Worker response · HTTP 200:
{
"envelope": {
"id": "b0db0353-6d5a-4a9b-b3f4-85f439ec4953",
"templateId": "8a821f8b-b0d1-4808-a92e-8d3f16e4228f",
"title": "Fixture agreement",
"subject": "Review the fixture",
"message": "Synthetic signing journey, no live agreement.",
"status": "sent",
"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": {
"documentId": "92c3741e-171c-439c-9419-597ffe485224",
"version": 1,
"sha256": "bc75a1b1ad4d41a275e3a4dadd9a6031e0c56029e486e47a181ba84891acc97f",
"pageCount": 1,
"pages": [
{
"width": 612,
"height": 792
}
]
},
"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.495Z",
"recipients": [
{
"id": "843e8126-a83e-471e-bc54-e57c07c0067d",
"envelopeId": "b0db0353-6d5a-4a9b-b3f4-85f439ec4953",
"role": "Signer",
"name": "Fixture Recipient",
"email": "recipient@example.test",
"status": "sent",
"routingOrder": 1,
"capacities": [
{
"key": "personal",
"kind": "personal"
}
],
"viewedAt": null,
"signedAt": null,
"declinedAt": null,
"typedName": null,
"evidenceSha256": null,
"linkIssuedAt": "2026-10-05T18:07:40.495Z",
"createdAt": "2026-10-05T18:07:40.489Z",
"updatedAt": "2026-10-05T18:07:40.495Z"
}
],
"auditEvents": [
{
"id": "fcd1c63e-81a4-4fd9-89fc-ba9b33f944de",
"envelopeId": "b0db0353-6d5a-4a9b-b3f4-85f439ec4953",
"recipientId": null,
"eventType": "envelope.sent",
"actorType": "user",
"actorEmail": "operator@example.test",
"metadata": {
"recipientCount": 1,
"fieldCount": 1,
"fieldSchemaSha256": "3a5a88731600386e198b8fe5915ebb0e6fba73e4326f1e0a3012de81293194c3",
"sourceDocumentId": "92c3741e-171c-439c-9419-597ffe485224",
"sourceDocumentVersion": 1,
"sourceSha256": "bc75a1b1ad4d41a275e3a4dadd9a6031e0c56029e486e47a181ba84891acc97f"
},
"createdAt": "2026-10-05T18:07:40.497Z"
},
{
"id": "57e62e82-a761-446c-980e-12689f481434",
"envelopeId": "b0db0353-6d5a-4a9b-b3f4-85f439ec4953",
"recipientId": null,
"eventType": "envelope.updated",
"actorType": "user",
"actorEmail": "operator@example.test",
"metadata": {
"recipientCount": 1,
"fieldCount": 1,
"sourceApp": null,
"sourceRef": null
},
"createdAt": "2026-10-05T18:07:40.490Z"
},
{
"id": "3693a899-7ef0-4e60-944b-c07d527bc692",
"envelopeId": "b0db0353-6d5a-4a9b-b3f4-85f439ec4953",
"recipientId": null,
"eventType": "envelope.updated",
"actorType": "user",
"actorEmail": "operator@example.test",
"metadata": {
"recipientCount": 0,
"fieldCount": 1,
"sourceApp": null,
"sourceRef": null
},
"createdAt": "2026-10-05T18:07:40.476Z"
},
{
"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"
}
]
},
"signingLinks": [
{
"recipientId": "843e8126-a83e-471e-bc54-e57c07c0067d",
"email": "recipient@example.test",
"routingOrder": 1,
"signingUrl": "https://sign.fixture.test/sign/6291be95d6257cc42cf869fdd1ca7c6d036cf1eeeaac623f02487c98848e33c4"
}
]
}
HTTP 200. Read $.signingLinks and confirm one link per unsigned recipient. Confirm $.envelope.source.sha256 is set: that is the document hash every signature binds to. Call envelopes.get and confirm each recipient status reads "sent". Do not call this action twice to "make sure" — a second send re-mints tokens and the links you already shared stop working.
Refused request · HTTP 401
Input · payload.json:
{
"envelopeId": "ed22fca5-d5f0-4392-8e71-96e247bf3875"
}
CLI:
topolo actions validate app_topolo_sign.envelopes.send --data @payload.json --json
topolo actions plan app_topolo_sign.envelopes.send --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:
{
"envelopeId": "ed22fca5-d5f0-4392-8e71-96e247bf3875"
}
CLI:
topolo actions validate app_topolo_sign.envelopes.send --data @payload.json --json
topolo actions plan app_topolo_sign.envelopes.send --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
- Read $.signingLinks and confirm one link per unsigned recipient.
- Confirm $.envelope.source.sha256 is set: that is the document hash every signature binds to.
- Call envelopes.get and confirm each recipient status reads "sent".
- Do not call this action twice to "make sure" — a second send re-mints tokens and the links you already shared stop working.
Errors and recovery
organization_required(HTTP 403): The token carries no organization scope. Re-authenticate against the organization that owns the workspace and retry.envelope_not_found(HTTP 404): No envelope in this workspace carries that id. Call envelopes.list and use an id from the response.envelope_closed(HTTP 409): The envelope is already completed, voided or declined and cannot be sent again. Create a new envelope if another signature is needed.document_required(HTTP 409): The envelope has no document. Set documentRef with envelopes.update before sending.message_required(HTTP 409): Both subject and message must be non-empty — they are what the signer receives. Set them with envelopes.update.fields_required(HTTP 409): The field schema is empty, so there would be nothing for the signer to complete. Set fieldSchema with envelopes.update.recipients_required(HTTP 409): The envelope has no recipients. Add at least one with envelopes.update before sending.document_not_ingested(HTTP 409): documentRef must name a document uploaded to Sign (sign-document:<id>) so it can be rendered, signed and flattened. Upload it with documents.upload and set documentRef to the returned $.document.documentRef.document_not_found(HTTP 404): No document in this workspace carries that sign-document id. Upload it with documents.upload.pdf_active_content(HTTP 422): The document contains JavaScript, launch/submit actions, XFA or embedded files. Export a plain PDF and upload it as a new version.field_assignee_unresolved(HTTP 409): detail names a field whose assignee role matches no recipient. Fix the field's assignee.role or the recipient's role with envelopes.update.field_assignee_required(HTTP 409): detail names a placed field with no assignee role on a multi-signer envelope. A box on the page belongs to exactly one signer; set assignee.role.recipient_roles_ambiguous(HTTP 409): detail names a placed field whose role matches more than one recipient. Give each recipient a distinct role.field_capacity_unresolved(HTTP 409): detail names a field assigned to a capacity the recipient does not hold. Add the capacity to the recipient or assign the field to "personal".recipient_signature_field_missing(HTTP 409): detail is role:capacity for a signer (or company capacity) with no signature or initials field. Add one so the signature is captured in that capacity.field_page_out_of_range(HTTP 409): detail names a field placed on a page the document does not have. Move it with envelopes.update.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
There is no unsend. Voiding invalidates the outstanding links so no further signature can be collected, but any recipient you already shared a link with may have read the terms. Check the document, fields and recipients before sending, not after.
Related actions
- 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.
- Get Sign audit certificate: Get the immutable audit certificate issued when an envelope completed: signer identities, capacities, consent and evidence hashes, the source and completed document SHA-256 and a hash chain over the audit trail, plus the certificate's own hash.
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.