Skip to main content

1. Prerequisites and configuration

1

1.1 - Create Tomorro superadmin account

An existing admin creates a dedicated superadmin account from Settings > Members (e.g. 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 a 403. See 3.4.
  • Recommended: no personal mailbox behind it, and exclude it from Tomorro notification settings.
2

1.2 - API key

Generate the API key from that superadmin account. It does not expire. It is sent on every REST call in the x-api-key header: Step 1: Get your API key
3

1.3 - Webhook setup in the admin console

https://app.tomorro.com/settings/integrations?integration=othersignaturetoolAdmin only, and one endpoint per workspace: the console edits a single configuration, there is no list and no “Add”. Every contract of the organization sent through the third-party option goes to that one URL.
  • 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.requested and signature.canceled are 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

1

Step 0 - Triggered by a user on Tomorro

Triggered when a user clicks “Send for signature” on a Tomorro contract set to the “Other signature tool” option (after the approval workflow if there is one).
Signature preparation in Tomorro, with Other signature tool selected as the signature provider
Send for signature dialog in Tomorro, with the Other signature tool badge
2

Step 1 - Receive the signature.requested webhook

Keep at least data.contractId, it is the key for every following call.Projected payload:
  • 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 the signature.canceled of a ceremony carries the same signatureId as its signature.requested, so you can tie them together beyond contractId.
3

Step 2 - Retrieve the files and signature information

Response:
  • downloadUrl is a pre-signed URL valid 5 minutes, see expiresAt. Download the file straight away and do not store the URL. Re-call the endpoint to get a fresh one.
  • isMain distinguishes the main document from its annexes, files[].order gives the order of the bundle.
  • Signing order: firstSigningParty says which side signs first (null when the signature is not ordered). Only internal signatories (member) are ranked among themselves, with signatories[].order. Guests are never ranked: their order is always null. In the example above: Alex, then Jane, then the guest. See 3.3.
  • fields gives 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 409 when 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 on GET /v2/contracts/{id}/signed-files.
View enum details in 3.3
4

Step 3 - Retrieve more contract information (optional)

Only needed if the third-party tool requires data that is not in the signature.requested payload or in signing-files (contract type, smart fields, dates, counterparty details).
5

Step 4 - Work in the third-party tool

Out of scope. Create the envelope, send it, wait for the signature.While the signature is running in the third-party tool, Tomorro sends nothing to the signatories: no request email, no reminder, no magic link. The contract simply sits in 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

1

Step 0 - Trigger on the third-party tool

Out of scope.
Steps 1 to 3 push the signed PDFs back to Tomorro. Each PDF (main document and each annex) goes through steps 1 and 2. Step 3 then runs once, with every file.
2

Step 1 - Get an upload URL, one per PDF

Response 201:
  • Read the values under data: data.url and data.key.
  • data.url is valid 5 minutes. Do step 2 straight away.
  • Keep data.key for step 3. Once the file is uploaded, the key stays usable until a successful step 3 consumes it.
3

Step 2 - Upload the PDF to data.url

This call goes to the storage URL, not to the Tomorro API.
  • 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-key and no Authorization header. The URL carries its own signature. An extra auth header gets the upload refused.
  • Content-Type must be application/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), whatever Content-Type was declared.
  • Use data.url exactly as returned, query string included. Do not decode or re-encode it.
  • Expected response: 200, empty body.
4

Step 3 - Push the contract to Tomorro

Response: 204 No Content, empty body. Do not try to parse JSON on success.
The body is strict: any other field is refused with a 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 signed and 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.signed webhook, syncs…
Errors on step 3On a 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.status is signed: done.
  • otherwise: send step 3 again with the same body. The keys are still valid, since only a 204 consumes them.
A 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

1

Step 0 - A user cancels the signature on Tomorro

Cancel signature process in the contract menu of a contract in Signing
2

Step 1 - Receive the signature.canceled webhook

Same envelope and same verification as 3.2.1 and 3.2.2, with 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.
3

Step 2 - Cancel in the third-party tool

Out of scope. Kill the envelope on the third-party side so the signatories stop receiving its reminders.No call back to Tomorro. The flow ends at step 2.The signature is already canceled on the Tomorro side, the contract is already back to 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

1

Step 0 - Trigger on the third-party tool

The signature will not complete: refused, canceled or expired in the third-party tool.Out of scope.
2

Step 1 - Cancel the signature in Tomorro

cancelReason is optional.
Tomorro runs the same flow as the cancel button in the app: the contract leaves 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.
3

Step 2 - Ignore the echo webhook

This cancellation also fires 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.
Steps 1 and 2 are the same upload as in Flow 2, for any accepted file type. Each attachment goes through steps 1 to 3 on its own. The same contentType is sent in all three steps, and the file name must end with the matching extension (.pdf for application/pdf, .xlsx for Excel…).
1

Step 1 - Get an upload URL, one per file

Replace application/pdf with the content type of your file (see Accepted file types).Response 201:
  • Read the values under data: data.url and data.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.url and data.key can be mapped in the next modules.
  • data.url is valid 5 minutes. Do step 2 straight away.
  • Keep data.key for 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.
2

Step 2 - Upload the file to data.url

This call goes to the storage URL, not to the Tomorro API.
  • The body is the raw file bytes: not multipart/form-data, not base64, not JSON.
    • n8n: HTTP Request node, method PUT, URL data.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.js await fetch(url, { method: 'PUT', headers: { 'Content-Type': contentType }, body: fileBuffer }).
  • No x-api-key and no Authorization header. 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-Type must be the same value as in step 1.
  • Use data.url exactly as returned, query string included. Do not decode or re-encode it.
  • Expected response: 200, empty body.
3

Step 3 - Attach the file to the contract

Response: 204 No Content, empty body. Do not try to parse JSON on success.
The body is strict and flat: the four fields sit at the root, with no 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 3A refused call changes nothing on the contract.RetriesA key is consumed by a successful step 3. If step 3 times out or answers 5xx, 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-version header, 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 data object (errors come as { error, meta }, see 3.4). Read data.url, data.files and so on. A 204 response has no body at all.

3.2 - Receiving webhooks

Full documentation: Technical documentation - Tomorro API

3.2.1 - Payload

Tomorro sends a POST 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

Header Leeway-Signature (also sent as Leeway_Signature):
Verification steps:
  1. Split the header on , to get t (timestamp in milliseconds) and sha256.
  2. 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.
  3. Compare in constant time with the sha256 value.
  4. Reject the call if the signature does not match, and reject a t that 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.requested that fails all 10 attempts is rolled back in Tomorro: the signature is canceled by Tomorro, the contract goes back to negotiating and the sender receives the usual “signature canceled” email asking to send again. No signature.canceled reaches 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 on POST /v2/contracts/{id}/signed-version, which accepts a contract in negotiating.
  • 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 on eventId, 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, same Leeway-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:
Branch on 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

Accepted file types For 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.

3.4 - Error handling

Every endpoint follows the standard error format of the REST API, no specific contract for this integration. Error envelope
  • error.statusCode is a string, not a number.
  • error.details is only present on validation errors, one entry per offending field.
  • meta.requestId is the value to quote to Tomorro support when reporting a failing call. Log it. It is also sent in the x-request-id response header.
Status codes (summary) Details per endpoint are in Flows 2, 4 and 5.

3.5 - Tomorro REST API Collection

Get the Tomorro OpenAPI collection

tomorro-openapi.json