Webhooks
Webhooks send an HTTPS POST to your server the moment a dispute arrives or changes status — so you can alert a channel, update a CRM, or trigger your own workflow without polling.
Add an endpoint
Section titled “Add an endpoint”- Open Settings → API & Webhooks and click Add endpoint.
- Enter a public HTTPS URL. Private addresses,
localhostand redirects aren’t allowed. - Choose the event types to send, or keep All events (including types added later).
- Leave Include customer details off unless the receiver needs the cardholder’s name, email and location.
- Save, and copy the signing secret (
whsec_…). You can view it again later with Signing secret. - Click Send test event to check your receiver responds with a
2xx.
A workspace can have up to 10 endpoints.
What we send
Section titled “What we send”Each delivery is a JSON event object with these headers:
| Header | Value |
|---|---|
Content-Type |
application/json |
User-Agent |
DisputeDash-Webhooks/1.0 |
DisputeDash-Signature |
t=<unix seconds>,v1=<hex HMAC> — see below |
DisputeDash-Event-Id |
The event’s id (evt_…) |
DisputeDash-Event-Type |
e.g. dispute.won |
DisputeDash-Delivery-Id |
Unique per delivery attempt chain |
The data.object is a snapshot of the dispute when the event happened. Test events have "livemode": false and a placeholder dispute.
Respond quickly
Section titled “Respond quickly”Return any 2xx status within 10 seconds. Anything else — a 3xx, 4xx, 5xx, a timeout or a connection error — counts as a failure. Do slow work (API calls, database writes) after responding, e.g. on a queue.
Verify the signature
Section titled “Verify the signature”Always verify that a request came from DisputeDash before acting on it.
- Read the raw request body exactly as received — before any JSON parsing.
- From
DisputeDash-Signature, taketand everyv1value. - Compute
HMAC-SHA256(secret, t + "." + rawBody)as lowercase hex. - Accept the request if it matches any
v1value (there are two for 24 hours after you rotate the secret), using a constant-time comparison. - Reject it if
tis more than 5 minutes from your clock, to block replays.
Node.js (Express)
import crypto from 'node:crypto'import express from 'express'
const app = express()const SECRET = process.env.DISPUTEDASH_WEBHOOK_SECRET
app.post('/webhooks/disputedash', express.raw({ type: 'application/json' }), (req, res) => { const header = req.get('DisputeDash-Signature') ?? '' const parts = header.split(',').map((p) => p.split('=')) const t = parts.find(([k]) => k === 't')?.[1] const sigs = parts.filter(([k]) => k === 'v1').map(([, v]) => v) if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.sendStatus(400)
const expected = crypto.createHmac('sha256', SECRET).update(`${t}.${req.body}`).digest('hex') const ok = sigs.some((s) => s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected))) if (!ok) return res.sendStatus(400)
const event = JSON.parse(req.body.toString('utf8')) res.sendStatus(200) // acknowledge first… handleEvent(event) // …then do the work})Python (Flask)
import hmac, hashlib, time, json, osfrom flask import Flask, request, abort
app = Flask(__name__)SECRET = os.environ["DISPUTEDASH_WEBHOOK_SECRET"].encode()
@app.post("/webhooks/disputedash")def disputedash(): raw = request.get_data() # raw bytes parts = [p.split("=", 1) for p in request.headers.get("DisputeDash-Signature", "").split(",")] t = next((v for k, v in parts if k == "t"), None) sigs = [v for k, v in parts if k == "v1"] if not t or abs(time.time() - int(t)) > 300: abort(400) expected = hmac.new(SECRET, f"{t}.".encode() + raw, hashlib.sha256).hexdigest() if not any(hmac.compare_digest(expected, s) for s in sigs): abort(400) event = json.loads(raw) # enqueue event for processing return "", 200Retries
Section titled “Retries”Failed deliveries are retried with increasing delays — after 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours, then daily — 9 attempts over roughly 3 days. After the last attempt the delivery is marked failed; you can Resend it from the endpoint’s delivery log.
If an endpoint keeps failing for 3 days, DisputeDash turns it off automatically and emails your workspace admins. Events that happen while an endpoint is off aren’t queued for it. Fix the receiver, send a test event, turn it back on, and use the events feed to catch up on anything you missed.
Duplicates and ordering
Section titled “Duplicates and ordering”Delivery is at least once and not guaranteed to be in order:
- The same event can arrive more than once (for example after a timeout where your server did process it). De-duplicate on
DisputeDash-Event-Id. - Events for the same dispute can arrive out of order. Use
data.object.updated_at(or the eventsequence) to ignore an older state that arrives after a newer one, or fetch the dispute from the API for its current state.
Rotate the secret
Section titled “Rotate the secret”Rotate secret creates a new signing secret immediately. For the next 24 hours each request carries signatures from both the new and the old secret, so you can deploy the new one without dropping events.
Delivery log
Section titled “Delivery log”Click Deliveries on an endpoint to see the last 30 days of deliveries: status, HTTP response code, duration, attempts, the next retry time, and the first 2 KB of your server’s response.
Manage endpoints with the API
Section titled “Manage endpoints with the API”Keys with the webhooks:manage scope can manage endpoints programmatically:
| Method & path | Does |
|---|---|
GET /v1/webhook_endpoints |
List endpoints. |
POST /v1/webhook_endpoints |
Create one. Body: url, optional description, events (array, or ["*"] for all), include_pii (boolean). The response includes the secret. |
GET /v1/webhook_endpoints/{id} |
Retrieve one. |
PATCH /v1/webhook_endpoints/{id} |
Change url, description, events, include_pii or enabled. |
DELETE /v1/webhook_endpoints/{id} |
Delete one. |
POST /v1/webhook_endpoints/{id}/rotate_secret |
Rotate the secret; returns the new secret and previous_secret_valid_until. |
POST /v1/webhook_endpoints/{id}/test |
Send a test event and return the result. |
curl https://app.disputedash.com/api/v1/webhook_endpoints \ -H "Authorization: Bearer $DISPUTEDASH_KEY" \ -H "Content-Type: application/json" \ -d '{"url":"https://example.com/webhooks/disputedash","events":["dispute.won","dispute.lost"]}'