Signing ceremony architecture
This page is intended for contributors and internal engineers. Partners do not interact with the signing ceremony directly — the Permissio App handles all signer-facing UX.
Call direction
Partner ──► Permissio API (POST /v1/envelopes, POST /v1/envelopes/:id/send, …)
Permissio App ──► Permissio API (POST /v1/internal/envelopes/:id/signers/:id/status)
When a partner calls POST /v1/envelopes/:id/send, the API server creates the
signing links and notifies the Permissio App. The signer then visits
sign.permissio.us to complete the ceremony. The Permissio App is the source
of truth for what the signer sees and submits.
Once a signer acts (views the document, signs, or declines), the Permissio App calls back into this API server via the internal endpoint to advance the signer's status. The API server then:
- Updates the signer row in the database.
- Emits a signer-level webhook event (
signer.viewed,signer.signed, orsigner.declined). - Recomputes the envelope's aggregate status and, if it changed, updates the envelope row and emits an envelope-level event.
Internal endpoint
POST /v1/internal/envelopes/{envelope_id}/signers/{signer_id}/status
Authorization: Bearer <INTERNAL_SIGNING_SECRET>
This route is not documented in the partner OpenAPI spec. It is protected
by a shared secret (INTERNAL_SIGNING_SECRET env var) and rejects any request
carrying a partner API key.
Status machine
Signer statuses
| From | To | Trigger |
|---|---|---|
pending | viewed | Signer opens the signing page |
pending | signed | Signer signs without first viewing (rare) |
pending | declined | Signer declines without first viewing |
viewed | signed | Signer submits their signature |
viewed | declined | Signer declines |
Envelope aggregate rules
After every signer status update the API server re-evaluates all signers with
role = "signer" (approvers and viewers are excluded):
| Condition | New envelope status | Event emitted |
|---|---|---|
Any required signer declined | declined | envelope.declined |
All required signers signed | processing | envelope.processing |
Some (not all) required signers signed | partially_signed | envelope.partially_signed |
What processing means
processing is an intermediate state between "all signers have signed" and
"the signed PDF is ready". The PDF stamp job runs asynchronously and moves the
envelope to completed when it finishes. That transition is handled by the PDF
pipeline — not by the signing ceremony endpoint.
Partners polling GET /v1/envelopes/:id will observe the processing status
and should wait for completed (or a envelope.completed webhook event) before
downloading the signed document.
Environment variable
| Variable | Description |
|---|---|
INTERNAL_SIGNING_SECRET | Shared secret between this API server and the Permissio App. Must be a long, high-entropy string. Set in Railway for both services. |