NewSign anything, sealed. Fill and sign any PDF in place, scans included, with every signed copy sealed.See what shipped
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

EventWhen it is sent
document.issuedA document is sent for signature
recipient.viewedA signer opens a document
recipient.signedSomeone signs
document.completedEveryone has signed
recipient.declinedSomeone declines to sign
document.voidedA document is withdrawn
document.recalledA document is recalled to be changed
invoice.issuedAn invoice is sent
invoice.viewedA customer opens an invoice
payment.recordedA 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.

Request
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.

Node.js (Express)
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
});
Python (Flask)
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 once

Answers 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 at and 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.