Developers · Webhooks
Events, signed and sent to your own endpoint.
When a document is sent, opened, signed, declined or withdrawn, or an invoice is sent, opened or paid, Qasha can post the event to your endpoint as JSON, signed with a secret only you and Qasha know.
Setting up
In Qasha, open Integrations and connect Webhooks with:
- Endpoint
- Where events are sent. It must start with https:// and can’t be an internal or private network address.
- Signing secret
- A secret of your choosing, kept encrypted in Qasha. Every request is signed with it.
- Events
- Which of the ten events this endpoint hears.
Events
| Event | When it is sent |
|---|---|
document.issued | A document is sent for signature |
recipient.viewed | A signer opens a document |
recipient.signed | Someone signs |
document.completed | Everyone has signed |
recipient.declined | Someone declines to sign |
document.voided | A document is withdrawn |
document.recalled | A document is recalled to be changed |
invoice.issued | An invoice is sent |
invoice.viewed | A customer opens an invoice |
payment.recorded | A payment is recorded |
The request
Each event is one POST with a JSON body. X-Qasha-Event names the event, and X-Qasha-Signature carries sha256= followed by the HMAC-SHA256 of the body with your signing secret, in hexadecimal.
POST /qasha-events HTTP/1.1
Host: example.com
Content-Type: application/json
X-Qasha-Event: recipient.signed
X-Qasha-Signature: sha256=5d0f2c…e91a
{
"event": "recipient.signed",
"description": "Ada Obi signed “Master services agreement”.",
"title": "Master services agreement",
"url": "https://<your-qasha-address>/?document=<document id>",
"details": { "name": "Ada Obi", "email": "ada@northwind.example" },
"at": "2026-11-06T10:42:07.512Z"
}- event
- The event’s name, the same as X-Qasha-Event.
- description
- One sentence saying what happened, ready to show a person.
- title
- The document’s title, or the invoice’s number.
- url
- A link to the document in Qasha. For invoice and payment events, a link to the workspace.
- details
- Whichever apply to the event: name and email (the signer), reason (for a decline), amount and currency (for a payment, with the amount in minor units such as cents), reference, and by (who acted).
- at
- When this delivery was made, in ISO 8601 UTC.
Verifying signatures
Compute the HMAC-SHA256 of the raw body, exactly as it arrived and before any parsing, with your signing secret. Compare it with the header in constant time, and refuse the request if they differ.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const secret = process.env.QASHA_SIGNING_SECRET;
// Check the signature against the raw body, exactly as it arrived.
app.post('/qasha-events', express.raw({ type: 'application/json' }), (req, res) => {
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(req.body).digest('hex');
const given = req.get('X-Qasha-Signature') || '';
const valid = given.length === expected.length && crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
if (!valid) return res.sendStatus(401);
const event = JSON.parse(req.body.toString('utf8'));
queue.add(event); // do the work later
res.sendStatus(204); // and answer at once
});import hashlib, hmac, os
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ["QASHA_SIGNING_SECRET"].encode()
@app.post("/qasha-events")
def qasha_events():
body = request.get_data() # the raw body, exactly as it arrived
expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-Qasha-Signature", "")):
abort(401)
queue.put(request.get_json()) # do the work later
return "", 204 # and answer at onceAnswers and retries
- Delivered
- Any 2xx answer within 5 seconds.
- Failed
- Any other answer, or none within 5 seconds.
- Retried
- No sooner than 5 minutes, 30 minutes, 2 hours, 12 hours and a day after each failure, in turn. Retries run while your team is using Qasha, so one can arrive later than its due time.
- Given up
- After the last retry fails. The last error stays on the integration, where the event can be sent again by hand.
Good practice
- Answer first, work later. Put the event on a queue and answer with a 2xx, well within the 5 seconds.
- Expect an event more than once. A delivery whose answer was lost can be sent again. Make your handler safe to run twice, for example by checking the document’s current state before acting.
- Don’t rely on order. Events can arrive out of order after a retry; use
atand the document’s state, not arrival order. - Rotate the secret with care. Change it in Qasha and at your endpoint together, or accept both while you switch.
Wire Qasha into the rest of your stack.
We’ll walk through the events your systems need and how to receive them safely.