1. Prerequisites and configuration
1.1 - Create Tomorro superadmin account
automation@customer.com), not a personal account.- The API key is bound to a member, not to the organization. Every write performed through the API is attributed to that member and inherits its permissions.
- Consequence:
- the contract activity log will show this account as the author of the signed-file import and of the cancellations. A named account such as “Signature automation” keeps the timeline readable.
- a superadmin has access to every contract of the workspace: the integration can query any of them (
GET,POST,PATCH) without the account being added as a participant. With any other role, the account only sees the contracts it takes part in, and the others are rejected with a403. See 3.4.
- Recommended: no personal mailbox behind it, and exclude it from Tomorro notification settings.
1.2 - API key
x-api-key header: Step 1: Get your API key1.3 - Webhook setup in the admin console
- Integration name (mandatory): the label displayed in the Tomorro UI in place of “Tomorro” or “Docusign” (e.g.
Yousign,Universign…). - URL (mandatory): the integration endpoint that will receive the events. HTTPS, publicly reachable.
- Secret: generated at creation, one single secret signs both events. Revealed on demand in the console. Editing the integration name or the URL does not rotate it, so an integration already deployed against it keeps verifying.
- Events:
signature.requestedandsignature.canceledare both subscribed automatically. They are shown for information and cannot be turned off separately. The integration needs both, since a signature that starts must also be able to be canceled. - Test buttons: one per event. Each sends a synthetic payload to the URL to validate reachability and signature verification before going live. See 3.2.4. Your endpoint must recognize these and ignore them.
2. Flows
Flow 1 - [Tomorro → Third-Party] Signature is requested
Step 0 - Triggered by a user on Tomorro


Step 1 - Receive the signature.requested webhook
data.contractId, it is the key for every following call.Projected payload:Notes
Notes
signatureNote: the message typed by the sender, to reuse as the envelope message.contractMembers: the Tomorro members of the contract, for instance to copy them on the envelope if your tool supports it.signatureId: the Tomorro signature this event is about. A cancel then re-send on the same contract opens a new one, and thesignature.canceledof a ceremony carries the samesignatureIdas itssignature.requested, so you can tie them together beyondcontractId.
Step 2 - Retrieve the files and signature information
Notes
Notes
downloadUrlis a pre-signed URL valid 5 minutes, seeexpiresAt. Download the file straight away and do not store the URL. Re-call the endpoint to get a fresh one.isMaindistinguishes the main document from its annexes,files[].ordergives the order of the bundle.- Signing order:
firstSigningPartysays which side signs first (nullwhen the signature is not ordered). Only internal signatories (member) are ranked among themselves, withsignatories[].order. Guests are never ranked: theirorderis alwaysnull. In the example above: Alex, then Jane, then the guest. See 3.3. fieldsgives the position of the signature zones placed in Tomorro. Use them, or let the third-party tool place its own. Coordinates are explained in 3.3.- Returns
409when there is no signature in progress on the contract: never sent for signature, canceled, or already signed. In the last case the executed PDFs are onGET /v2/contracts/{id}/signed-files.
Step 3 - Retrieve more contract information (optional)
signature.requested payload or in signing-files (contract type, smart fields, dates, counterparty details).Step 4 - Work in the third-party tool
Signing.Keep the link to the contract. Store contractId on the envelope (external id, metadata or custom field, depending on your tool), or keep your own contractId ↔ envelope table. Flows 2 and 4 need it to know which contract to call back, and Flow 3 needs it to find the envelope to kill.Flow 2 - [Third-Party → Tomorro] Signature is completed
Step 0 - Trigger on the third-party tool
Step 1 - Get an upload URL, one per PDF
201:Notes
Notes
- Read the values under
data:data.urlanddata.key. data.urlis valid 5 minutes. Do step 2 straight away.- Keep
data.keyfor step 3. Once the file is uploaded, the key stays usable until a successful step 3 consumes it.
Step 2 - Upload the PDF to data.url
Notes
Notes
- The body is the raw file bytes: not
multipart/form-data, not base64, not JSON.- n8n: HTTP Request node, body content type n8n Binary File.
- Make: HTTP module, send the file data as the raw request body.
- Code:
curl -X PUT --data-binary @signed.pdf -H "Content-Type: application/pdf" "<data.url>"
- No
x-api-keyand noAuthorizationheader. The URL carries its own signature. An extra auth header gets the upload refused. Content-Typemust beapplication/pdf, the same value as in step 1.- Send the real PDF. Storage accepts any bytes here, but Tomorro reads the uploaded file in step 3 and refuses anything that is not a PDF (
400 SignedFileNotPdf), whateverContent-Typewas declared. - Use
data.urlexactly as returned, query string included. Do not decode or re-encode it. - Expected response:
200, empty body.
Step 3 - Push the contract to Tomorro
204 No Content, empty body. Do not try to parse JSON on success.Notes
Notes
400. That includes contractId, which already goes in the URL.What Tomorro does on receipt:- attaches the PDFs as the contract’s signed files, and marks the document that was sent for signature as signed
- marks every signatory as signed on
signatureDate - moves the contract to
signedand stamps its signature date. If the contract type sets start date = signature date, the start date takes the same value - runs the usual post-signature automations: notification to the contract members,
contract.signedwebhook, syncs…
409, error.message only says Conflict: branch on error.errorId.A refused call (400 or 409) changes nothing on the contract, and the keys stay usable.RetriesIf step 3 times out or answers 5xx, you cannot tell whether it went through. Call GET /v2/contracts/{contractId}:data.statusissigned: done.- otherwise: send step 3 again with the same body. The keys are still valid, since only a
204consumes them.
409 ContractAlreadySigned on a retry means the first call worked.Also usable outside this flowThe same endpoint accepts a contract in draft or negotiating that was never sent for signature, for instance to import a contract signed elsewhere. The first PDF then becomes a new version of the document, marked signed. Everything else is identical.Flow 3 - [Tomorro → Third-Party] Signature canceled in Tomorro
Step 0 - A user cancels the signature on Tomorro

Step 1 - Receive the signature.canceled webhook
eventType = signature.canceled.data carries one field the signature.requested payload does not: cancelReason, the reason typed by whoever canceled in Tomorro. It is a string, or null when no reason was given. Carry it into the cancellation on your side rather than voiding the envelope with no explanation.Step 2 - Cancel in the third-party tool
draft or negotiating. Do not call cancel-signatures here. It would be rejected with a 400, since the contract is no longer in signing.Expect the echo from your tool. Killing the envelope usually makes your tool fire its own “canceled” event, which starts your Flow 4. There, cancel-signatures answers 400 because the contract is no longer in signing: this is expected, treat it as success. To skip the extra call, ignore that event in Flow 4 when the cancel came from Tomorro.Flow 4 - [Third-Party → Tomorro] Signature canceled in the third-party tool
Step 0 - Trigger on the third-party tool
Step 1 - Cancel the signature in Tomorro
cancelReason is optional.Notes
Notes
Signing, goes back to draft or negotiating, and the contract participants are notified.Response: 204 No Content, empty body. Rejected with a 400 if the contract is not in signing (never sent for signature, already signed or canceled).Retry: if the call times out or answers 5xx, send it again. A 400 on that retry means the first call went through: treat it as success.Step 2 - Ignore the echo webhook
signature.canceled to your endpoint, exactly like a cancel made in Tomorro (Flow 3). The envelope is already dead on your side: verify the signature, answer 2xx, and do nothing else.Flow 5 - [Third-Party → Tomorro] Add attachments (optional)
Adds a file to the contract’s attachments, next to its signed document. Typically the signature certificate when your tool does not put it in the signed bundle, or any supporting document it produces (audit trail, evidence file…).- When: usually right after Flow 2 has answered
204. The endpoint works on a contract in any status and never changes the status. - One file per call. Unlike
signed-version, which takes every PDF in one call, each attachment runs through steps 1, 2 and 3 on its own. - Not only PDFs: PDF, Word, Excel and more. See Accepted file types in 3.3.
contentType is sent in all three steps, and the file name must end with the matching extension (.pdf for application/pdf, .xlsx for Excel…).Step 1 - Get an upload URL, one per file
application/pdf with the content type of your file (see Accepted file types).Response 201:Notes
Notes
- Read the values under
data:data.urlanddata.key.- n8n: in the following nodes, reference them by the node name, e.g.
{{ $('Get upload URL').item.json.data.key }}. - Make: enable Parse response on the HTTP module so
data.urlanddata.keycan be mapped in the next modules.
- n8n: in the following nodes, reference them by the node name, e.g.
data.urlis valid 5 minutes. Do step 2 straight away.- Keep
data.keyfor step 3. Once the file is uploaded, the key stays usable until a successful step 3 consumes it. Pass it unchanged (no trimming, no leading/, not the URL): one key = one file = one attachment. - A content type that is not accepted is already refused here with
400 FileContentTypeNotAllowed.
Step 2 - Upload the file to data.url
Notes
Notes
- The body is the raw file bytes: not
multipart/form-data, not base64, not JSON.- n8n: HTTP Request node, method
PUT, URLdata.url, body content type n8n Binary File, pointing at the binary property that holds the file. - Make: HTTP module, method
PUT, send the file data as the raw request body. - Code:
curl -X PUT --data-binary @certificate.pdf -H "Content-Type: application/pdf" "<data.url>", or in Node.jsawait fetch(url, { method: 'PUT', headers: { 'Content-Type': contentType }, body: fileBuffer }).
- n8n: HTTP Request node, method
- No
x-api-keyand noAuthorizationheader. The URL carries its own signature. An extra auth header gets the upload refused. In n8n and Make, make sure this node does not reuse the Tomorro credential (authentication: none). Content-Typemust be the same value as in step 1.- Use
data.urlexactly as returned, query string included. Do not decode or re-encode it. - Expected response:
200, empty body.
Step 3 - Attach the file to the contract
204 No Content, empty body. Do not try to parse JSON on success.Notes
Notes
file wrapper. Any other field is refused with a 400, including contractId, which already goes in the URL.See Accepted file types below in 3.3.Errors on step 35xx, retry once with the same body: if the first call actually went through, the retry is refused and the file is already on the contract. Do not start over from step 1 in that case, or the file ends up attached twice.Also usable outside this flowThe endpoint is not tied to the signature: any integration can use it to add a supporting document to any contract, in any status.3. Technical references
Reference material: conventions, webhook mechanics, enums and errors. Read it once, then come back to it while implementing the flows above.3.1 - API conventions
- REST only. The whole integration runs on the public REST API.
- Base URL:
https://api.tomorro.com/v2 - Authentication:
x-api-key: <your api key>on every call. A missing or unknown key returns 401. - Versioning: optional
tomorro-versionheader, defaults to the latest version. - Rate limit: 40 calls per minute, well above what this integration needs.
- Sandbox: available on request. It is an empty sandbox, not a copy of your production workspace.
- Content type:
application/json - Response envelope: every successful JSON response is wrapped in a
dataobject (errors come as{ error, meta }, see 3.4). Readdata.url,data.filesand so on. A204response has no body at all.
3.2 - Receiving webhooks
Full documentation: Technical documentation - Tomorro API3.2.1 - Payload
Tomorro sends aPOST to the configured URL. Both events use the same envelope and the same data shape, the business payload sits under data.
These webhooks are organization-based, meaning every contract sent for signature through the third-party option will fire.
A delivery triggered by the test buttons in the admin console carries one extra top-level field, "test": true, absent from every genuine event. See 3.2.4.
3.2.2 - Signature verification
HeaderLeeway-Signature (also sent as Leeway_Signature):
- Split the header on
,to gett(timestamp in milliseconds) andsha256. - Recompute
HMAC-SHA256(secret, "<t>.<raw body>")in hexadecimal. Use the raw body, not a re-serialized JSON object, otherwise the hash will not match. - Compare in constant time with the
sha256value. - Reject the call if the signature does not match, and reject a
tthat is too old to protect against replay (5 minutes is a reasonable window).
3.2.3 - Acknowledgement and retries
- Timeout 3000 ms. The endpoint must answer 2xx in under 3 seconds. Acknowledge first, do the work asynchronously.
- Any non-2xx answer or timeout triggers a retry: 10 attempts in total (first delivery + 9 retries), 5 minutes apart, about 45 minutes. The count is per event, but reaching it disables the whole webhook.
- After the 10th failure the webhook is automatically disabled (
enabled = false). It has to be re-enabled in the admin console, and the “Other signature tool” option disappears from the UI while it is disabled. A contract that was already switched to the provider keeps it listed, but cannot be sent for signature until the endpoint is active again. - A
signature.requestedthat fails all 10 attempts is rolled back in Tomorro: the signature is canceled by Tomorro, the contract goes back tonegotiatingand the sender receives the usual “signature canceled” email asking to send again. Nosignature.canceledreaches you, since the webhook is disabled by then. If your tool did create the envelope anyway (answer later than 3 s), you can still push the signed PDFs onPOST /v2/contracts/{id}/signed-version, which accepts a contract innegotiating. - Events fired while the webhook is disabled are dropped, not queued and not replayed on re-enable. A signature canceled in Tomorro during that window never reaches your endpoint: after re-enabling, reconcile the contracts you had in
signing. - Idempotency: a retry resends the same
eventId. Deduplicate oneventId, an event can legitimately arrive several times.
3.2.4 - Test fires from the admin console
The Send a test buttons in the admin console fire a real, signed delivery at your URL: same envelope, sameLeeway-Signature header, same 3 s timeout. The body is synthetic: its ids name no real contract, so every API call made with them is rejected.
A test fire is recognizable by one extra top-level field, which never appears on a genuine event:
test: verify the signature, answer 2xx, and stop there.
The signature.canceled test is the same body with "eventType": "signature.canceled" and data.cancelReason. Tests are not retried and do not count towards the 10 failures that disable the webhook (3.2.3).
3.3 - Enum and field reference
files/upload-url and attachments (Flow 5). The name must end with the extension on the same row, and the same contentType is sent in the three steps. signed-version (Flow 2) only accepts PDF.
Other accepted types
Other accepted types
3.4 - Error handling
Every endpoint follows the standard error format of the REST API, no specific contract for this integration. Error envelopeerror.statusCodeis a string, not a number.error.detailsis only present on validation errors, one entry per offending field.meta.requestIdis the value to quote to Tomorro support when reporting a failing call. Log it. It is also sent in thex-request-idresponse header.
3.5 - Tomorro REST API Collection
Get the Tomorro OpenAPI collection
tomorro-openapi.json