The nine o'clock spreadsheet

Every morning at a fuel supplier, someone exported a CSV of completed envelopes from the signing platform and pasted it into a sheet next to the CRM export, to work out which supply contracts had come back signed overnight so account managers could start delivery.

Forty minutes, by hand, because the two systems had no idea the other existed. The CRM knew a contract was needed. The signing platform knew it had been signed. Nothing carried that fact between them.

That gap is the whole problem, and it is smaller than it looks.

The integration surface is deliberately small

Most contract automation needs two things: send a document out for signature, then find out what happened to it. That is one endpoint and a webhook.

There is no sprawling REST API here, and we would rather say so than pad the list. POST /api/signing-sessions/start creates an envelope, uploads the PDFs, positions the fields and emails the signers. Webhooks report when a signer has signed and when the envelope has closed. Customers run production contract flows on that pair today.

Everything else, listing envelopes, downloading signed files, managing templates, reading the audit trail, runs from a signed-in user session rather than a server key. Knowing that up front saves you designing against endpoints that are not there.

Getting a key

API keys live in the dashboard under Developers → API keys. The secret appears once, at creation, and cannot be retrieved afterwards, so copy it into your secret store before leaving the page. Live keys are prefixed sk_live_, test keys sk_test_.

A key belongs to the organization, not to a person. Envelopes it creates, usage it records and plan limits it consumes all belong to the organization, so a key survives the developer who made it leaving. Revoking one takes effect immediately and further calls return 401.

A key can never create another key: key management, billing and organization settings all require a user session.

Sending the contract

The endpoint takes multipart/form-data, because it is carrying PDFs.

curl -X POST https://api.sign.wallid.io/api/signing-sessions/start \
  -H "x-api-key: sk_live_..." \
  -F "files=@./contract.pdf" \
  -F 'recipients=[{"email":"signer@company.com","name":"Signer"}]'

Two fields are required. files carries one or more PDFs, with multiple files parts allowed for a multi document envelope. recipients is a JSON string holding the array of signers.

The optional fields cover the rest. documentsMeta maps each uploaded file to a stable documentId, which is what signature positions point at. envelopeName is the display name. envelopeId lets you supply your own UUID rather than take a generated one: mint it in your CRM and you hold the correlation key before the request is sent. appId is for the kiosk flow, and when it is set no invitation emails go out.

Each signer object carries email, name, color, signatureType, notificationType and a signatures array whose entries hold id, x, y, page, documentId, width and height. Coordinates are in PDF space, origin at the bottom left, which catches people converting from a browser viewer.

signatureType is set per signer, at envelope creation, so a contract where the counterparty signs with Chave Móvel Digital and an internal approver uses a simpler flow is one envelope, not two.

Order is controlled by isSequentialSigning, a boolean defaulting to false. Left alone, everyone is notified at once. Set to true, only the first signer is notified, and each subsequent person when the one before them finishes. The per signer position is signingOrder. Choosing between sequential and parallel signing is more about approval chains than about code.

What triggers the send

This varies most, and it is worth deciding before you write the handler. A deal stage moving to "Contract Out". An ERP job reaching approved status. A nightly batch of renewals. An operator pressing a button on a record.

Each implies something different about idempotency, about who owns the envelope UUID, and about what happens if the call fails at 3am with nobody watching. If you are working out where the trigger belongs in your own stack, that is worth talking through before the first line of code.

Talk to our team →

Getting the result back

Register an HTTPS endpoint per organization under Developers → Webhooks. As with API keys, the signing secret is shown once at creation. There are four events, and that is the complete list.

EventWhat happenedWhat your system should do
signer.signedA signer completed their signatureUpdate the contract record, advance the deal stage, notify the account manager
signer.failedA signer's signature failed technicallyFlag for follow up so someone can help that signer through it again
envelope.completedEvery signer has signed, envelope closedMark the contract executed, trigger provisioning, delivery or invoicing
pingThe dashboard Test button was pressedReturn 200 so you can confirm the endpoint is reachable

signer.failed is easy to misread. It means the signature process broke: an authentication step did not complete, a provider call errored, something went wrong mid flow. It is an operational alert, not an outcome of the deal.

Every delivery carries the same payload shape.

{
  "event": "envelope.completed",
  "createdAt": "2026-07-22T09:00:00.000Z",
  "organizationId": "507f1f77bcf86cd799439011",
  "data": {
    "envelopeId": "550e8400-e29b-41d4-a716-446655440000",
    "signingSessionId": "507f1f77bcf86cd799439012",
    "status": "completed"
  }
}

For the two signer.* events, data also carries signerId, signerEmail and signatureType. A single endpoint receives every event type, so you branch on event. The envelopeId is your join key back to whatever record started this, which is why supplying your own UUID at creation pays off.

Verifying the signature, and the mistake everyone makes

Four headers arrive with each request. X-WalliD-Event is the event name. X-WalliD-Delivery is a unique delivery id: store it and drop duplicates, because retries are real. X-WalliD-Timestamp is epoch milliseconds at signing time. X-WalliD-Signature is sha256=<hex>.

The signature is an HMAC-SHA256 over <timestamp>.<raw body> using your endpoint secret. The word doing the work there is raw. If your framework has already parsed the JSON and you re-serialise it to verify, key order and whitespace differ from what was signed, and verification fails on a perfectly valid payload. It is the most common webhook integration bug.

const crypto = require('crypto');

function isValid(req, secret) {
  const timestamp = req.headers['x-wallid-timestamp'];
  const signature = req.headers['x-wallid-signature'];
  const expected =
    'sha256=' +
    crypto
      .createHmac('sha256', secret)
      .update(`${timestamp}.${req.rawBody}`)
      .digest('hex');
  return signature === expected;
}

In Express, that means capturing rawBody in the verify callback of the JSON body parser, before the parsed object exists.

The timestamp sits inside the signed value, which stops a captured payload being replayed later under a fresh one.

Behaving well as a consumer

Reply 2xx to acknowledge. Anything else is a failure, and so is taking too long. Verify the signature, enqueue the work, return 200, and let your own worker do the slow parts.

Failed deliveries are retried with exponential backoff, up to 5 attempts. Repeated consecutive failures trigger a warning email to the organization's owners and admins, and an endpoint that keeps failing is automatically disabled until you re-enable it. Any successful delivery resets the counter, so a ten minute deploy window is harmless and a handler throwing since Tuesday is not.

Endpoints must be public HTTPS. Localhost and private or internal addresses are not accepted, standard SSRF protection, so you need a tunnel to test locally. The delivery log is retained for 90 days.

Things you get without building them

Templates. Reusable documents with field positions already placed, and signer roles that are either fixed people or placeholders filled in at send time. For a contract you issue two hundred times a month, this takes field positioning out of your integration entirely.

Applications. Named destinations for a completed document: an email address, a webhook, or an FTP location, with credentials stored encrypted.

Audit trail. Per signer: notifiedAt, firstViewedAt, lastViewedAt, linkAccessCount, signedAt, IP address, user agent, and SHA-256 hashes of the original and the signed document. A signed PDF can be looked up by its hash to find its envelope, which answers "someone sent us this file, is it real".

Where the signer is in front of you rather than reading an email, see in-person signing at a counter or tablet.

Article 25(1) of the eIDAS Regulation (EU) 910/2014: "An electronic signature shall not be denied legal effect and admissibility as evidence in legal proceedings solely on the grounds that it is in an electronic form or that it does not meet the requirements for qualified electronic signatures."

Which assurance level a contract needs is answered by the contract and the jurisdiction, not by the API.

Frequently asked questions

How do I send a contract for signature from my CRM? Call POST /api/signing-sessions/start with an API key in the x-api-key header, as multipart/form-data. Attach the PDF as files and the signer list as a JSON string in recipients. Each signer is emailed a unique signing link.

What events does the electronic signature webhook send? Four: signer.signed, signer.failed when a signature fails technically, envelope.completed when every signer has signed, and ping from the dashboard Test button.

How do I verify a webhook signature? Compute an HMAC-SHA256 over <timestamp>.<raw body> using your endpoint secret, taking the timestamp from X-WalliD-Timestamp, then compare against X-WalliD-Signature in the form sha256=<hex>. Use the raw body, not a re-serialised parsed object, or it will never match.

Can I use an API key to check envelope status or download the signed PDF? No. An API key authenticates integration endpoints only, currently just envelope creation. Listing sessions, downloading documents and reading the audit trail need a signed-in user session. Webhooks are how your server learns about state changes.

What happens if my webhook endpoint is down? The delivery is retried with exponential backoff, up to 5 attempts. Repeated consecutive failures send a warning email to the organization's owners and admins, and a persistently failing endpoint is disabled until re-enabled in the dashboard. Any success resets the counter.

Can different signers on one contract use different signature methods? Yes. signatureType is set per signer when the envelope is created, so one envelope can combine a qualified signature from the counterparty with a simpler flow for an internal approver.


Working out how envelope creation should hook into your CRM, or which events your back office needs to consume? Talk to us.

Talk to our team →