Skip to content

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.

  1. Open Settings → API & Webhooks and click Add endpoint.
  2. Enter a public HTTPS URL. Private addresses, localhost and redirects aren’t allowed.
  3. Choose the event types to send, or keep All events (including types added later).
  4. Leave Include customer details off unless the receiver needs the cardholder’s name, email and location.
  5. Save, and copy the signing secret (whsec_…). You can view it again later with Signing secret.
  6. Click Send test event to check your receiver responds with a 2xx.

A workspace can have up to 10 endpoints.

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.

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.

Always verify that a request came from DisputeDash before acting on it.

  1. Read the raw request body exactly as received — before any JSON parsing.
  2. From DisputeDash-Signature, take t and every v1 value.
  3. Compute HMAC-SHA256(secret, t + "." + rawBody) as lowercase hex.
  4. Accept the request if it matches any v1 value (there are two for 24 hours after you rotate the secret), using a constant-time comparison.
  5. Reject it if t is 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, os
from 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 "", 200

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.

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 event sequence) to ignore an older state that arrives after a newer one, or fetch the dispute from the API for its current state.

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.

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.

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.
Terminal window
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"]}'