Guide to Justt Webhooks: Features and Best Practices
Justt fights chargebacks on behalf of merchants. It pulls disputes in from your payment providers, builds and submits the evidence, handles pre-chargeback alerts from CDRN, RDR and Ethoca, and can cancel a customer's subscription in your billing system after a refund. Its webhooks report each of those steps to your own systems, so finance, support and fraud tooling see the same dispute state Justt does.
This guide walks through Justt's 11 event types and what each data object contains, endpoint setup, signature verification, and the places where Justt's own documentation disagrees with itself.
What are Justt webhooks?
Justt's webhook feature isn't enabled for every customer yet; if you don't see a Webhooks tab, ask Justt. Where it is enabled, you add endpoints in the dashboard, and each endpoint gets the event types you choose, or all of them if you choose none. Justt suggests one endpoint per service.
Requests are JSON POSTs with webhook-id, webhook-timestamp and webhook-signature headers, signed per Standard Webhooks. The body is {type, timestamp, data}. The dashboard, the msg_ message IDs and the resend and recovery tools all match what Svix provides.
Justt webhook features
| Feature | Details |
|---|---|
| Availability | Not yet enabled for every customer |
| Configuration | Account level view > Webhooks tab |
| Subscription model | Selected types, or all types when none are selected |
| Signature | Standard Webhooks HMAC-SHA256 |
| Headers | webhook-id (msg_...), webhook-timestamp, webhook-signature |
| Secret | Signing Secret per endpoint (prefix not printed; the sample implies whsec_) |
| Body | {type, timestamp, data} |
| Event routing | Body field type |
| Response window | 15 seconds (the API reference says 30) |
| Retries | 8 attempts over about 27.6 hours (the API reference says up to 3) |
| Permanent failure | 410 Gone stops retries |
| Disabling | After 5 consecutive days of failures |
| Recovery | Resend one message, or recover failures since a date |
| Transformations | Can rewrite payloads or routing before delivery |
| Source IPs | Not published |
Events and payloads
| Field | Meaning |
|---|---|
type | The event type, for example chargeback.created. This is what handlers switch on |
timestamp | ISO 8601 time the event occurred |
data | Event-specific object (see below) |
Common Event Types
| Event | Triggered When | Common Use Cases |
|---|---|---|
chargeback.created | A new chargeback is received from any connected PSP | Open a case, alert the team |
chargeback.updated | A chargeback is updated | Refresh your copy of the record |
chargeback.status.updated | A chargeback status update is received | Track the dispute lifecycle |
chargeback.evidence.submitted | Evidence is submitted to the PSP; data.status carries the outcome | Confirm the dispute response, retry on failed |
chargeback.accepted | A chargeback acceptance is confirmed by the PSP; data.status carries the outcome | Write off the amount |
pre_ | A pre-chargeback alert arrives from CDRN, RDR or Ethoca | Decide whether to refund |
pre_ | The transaction linked to a refunded alert was refunded through the PSP | Close the alert |
pre_ | A refund alert could not be processed | Refund manually |
pre_ | An inbound alert was matched to its transaction. Opt-in, enabled by Justt per account | Link the alert to your order |
subscription.cancel_ | A CRM subscription (for example Chargebee or Recurly) was cancelled after a refund | Update entitlements |
subscription.cancel_ | The cancellation failed (subscription not found, already cancelled). Justt flags the alert as Action Required | Cancel by hand |
Justt calls these its supported core event types, so treat the list as a minimum.
data by event family
| Events | Notable data fields |
|---|---|
chargeback.created, chargeback.updated, chargeback.status.updated | The full chargeback: id, chargebackId, referenceAccountId, psp, currency, amount, status, pspStatus, lifecycleStage, reasonCode, reason, reasonGroup, cardScheme, dueDate, winProbability and more |
chargeback.evidence.submitted | submitEvidenceId, fileId, id, chargebackId, status (succeeded or failed), submittedAt, error |
chargeback.accepted | id, chargebackId, status (succeeded or failed), acceptedAt, error |
pre_, pre_ | internalAlertId, provider (CDRN, RDR or Ethoca), providerAlertId, alertType, alertTimestamp, arn, transaction; matched events add matchedTransaction |
pre_, pre_ | internalAlertId, providerName, providerAlertId, psp (pspName, transactionId, plus refundId / refundedDate or refundError), amount, currency |
subscription.cancel_, subscription.cancel_ | subscriptionId, transactionId, alertId, chargebackId, crmProvider; failures add failureCode and failureReason |
Here is the chargeback.accepted sample from the webhook samples library, taken from Justt's reference:
{
"type": "chargeback.accepted",
"timestamp": "2022-03-15T09:39:24.514Z",
"data": {
"id": "04318fb0-e4ff-4712-972e-4db99b3fb23c",
"chargebackId": "QRZ54SXB4K6TF232",
"referenceAccountId": "04318fb0-e4ff-4712-972e-4db99b3fb23c",
"status": "inProgress",
"acceptedAt": "2025-01-15T12:34:56.789Z",
"error": "Network timeout",
"externalTransactionIdentifier": "ext_txn_123"
}
}
The example's status of inProgress doesn't match the documented succeeded or failed, which is another reason to handle unexpected values rather than assume two outcomes. The body has no event ID; deduplicate on webhook-id.
Setting up Justt webhooks
- In the Justt app, switch to the Account level view and open Webhooks (
https://app.justt.ai/aggregated/webhooks). - Add an endpoint with a public HTTPS URL that accepts
POSTand replies within 15 seconds. - Select event types. Selecting none subscribes to everything, which is useful while testing; in production choose the types you handle.
- Save, open the endpoint, and copy its Signing Secret into
JUSTT_WEBHOOK_SECRET.
Justt never prints the secret's prefix. Its sample code base64-decodes whatever follows the first _, which fits the whsec_<base64> form; check that yours starts with whsec_. During development, the Hookdeck CLI can give your local receiver a public URL:
npx hookdeck-cli listen 3000 justt --path /webhooks/justt
Justt also offers webhook transformations, which can rewrite the payload or its routing before delivery. If you use one, verify and parse what actually arrives.
With Hookdeck. Add a Justt source in Hookdeck with the endpoint's signing secret and register the source URL as the Justt endpoint. It takes POST only. Hookdeck returns 200 to Justt for each request, and a request that doesn't verify stays in Hookdeck as failed verification instead of reaching your service.
Securing Justt webhooks
Justt's signature page prints the construction: {webhook-id}.{webhook-timestamp}.{raw body}, HMAC-SHA256 with the base64-decoded secret, compared as v1,<base64>. It stresses that the signature covers the exact raw body. The function below follows those steps and accepts any matching v1 entry:
const crypto = require('node:crypto');
function decodeBase64(value) {
if (typeof value !== 'string' || !/^[A-Za-z0-9+/]+={0,2}$/.test(value)) return null;
const bytes = Buffer.from(value, 'base64');
return bytes.length > 0 && bytes.toString('base64').replace(/=+$/, '') === value.replace(/=+$/, '')
? bytes : null;
}
function verifySignature(rawBody, headers, secret) {
if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
const prefix = headers['webhook-id'] !== undefined ? 'webhook' : 'svix';
const id = headers[`${prefix}-id`];
const timestamp = headers[`${prefix}-timestamp`];
const signature = headers[`${prefix}-signature`];
if (typeof id !== 'string' || !id || typeof timestamp !== 'string' ||
!/^[0-9]+$/.test(timestamp) || typeof signature !== 'string') return false;
const key = decodeBase64(secret.replace(/^whsec_/, ''));
if (!key) return false;
const message = Buffer.concat([Buffer.from(`${id}.${timestamp}.`, 'utf8'), rawBody]);
const candidates = signature.split(/\s+/).filter((item) => item.startsWith('v1,')).map((item) => item.slice(3));
const expected = crypto.createHmac('sha256', key).update(message).digest();
return candidates.some((candidate) => {
const received = decodeBase64(candidate);
if (!received) return false;
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
Justt's sample on that page signs a ping body that uses event_type rather than type. No ping event is listed and every reference payload uses type, so route on type and acknowledge anything else:
const express = require('express');
// Returns the parsed body when it is a JSON object, otherwise null.
function parseJsonObject(rawBody) {
try {
const value = JSON.parse(rawBody.toString('utf8'));
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
} catch {
return null;
}
}
const app = express();
app.post('/webhooks/justt', express.raw({ type: '*/*' }), async (req, res) => {
const secret = process.env.JUSTT_WEBHOOK_SECRET;
if (!secret) return res.sendStatus(500);
if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);
const event = parseJsonObject(req.body);
if (!event) return res.sendStatus(400); // signed, but not a JSON object
if (typeof event.type !== 'string') return res.sendStatus(400);
const messageId = req.headers['webhook-id'];
try {
if (event.type === 'pre_chargeback_alert.refund.failed' || event.type === 'subscription.cancel_immediately.failed') {
await opsQueue.add({ messageId, type: event.type, data: event.data }); // needs a person
} else if (event.type.startsWith('chargeback.')) {
await disputes.apply(messageId, event.type, event.data);
}
} catch {
return res.sendStatus(503); // not processed, so don't acknowledge
}
return res.sendStatus(200);
});
opsQueue and disputes stand in for your own code.
Justt recommends rejecting timestamps more than 5 minutes old, and the Standard Webhooks libraries do that by default. Justt's resends and recoveries keep the original timestamp, and so do Hookdeck's retries and replays; Hookdeck itself sets no tolerance. A receiver behind Hookdeck should skip the age check, as above, and deduplicate on webhook-id.
Make Justt webhooks production-ready. Hookdeck Event Gateway verifies Justt's signatures, records each chargeback and alert event, and routes the ones that need action to the right team's service.
Justt webhook limitations and pain points
Conflicting timeout and retry figures
The Problem: You can't be sure how long you have to respond or how many retries to expect.
Why It Happens: Justt's retry page gives a 15-second timeout and 8 attempts over about 27.6 hours, with endpoints disabled after 5 days of failures. The API reference says 30 seconds and up to 3 retries.
Workarounds:
- Design for the stricter numbers: answer within 15 seconds and assume few retries.
- Use the dashboard's recover tool after any outage.
How Hookdeck Can Help: Hookdeck answers Justt at once and runs its own retry policy to your service, so the discrepancy stops mattering.
Failures that need a person
The Problem: A refund or subscription cancellation fails and nobody notices.
Why It Happens: pre_ and subscription.cancel_ report work Justt couldn't finish. Justt marks the alert Action Required in its own app, but your team may not be watching there.
Workarounds:
- Route these events to an alerting channel, not just a database.
How Hookdeck Can Help: Hookdeck filters can send the failure types to a dedicated destination, such as an on-call webhook.
410 turns retries off
The Problem: A deploy that briefly returns 410 Gone loses events permanently.
Why It Happens: Justt treats 410 as a permanent failure and stops retrying that message.
Workarounds:
- Never return
410from the webhook route; use404or503for temporary problems.
How Hookdeck Can Help: With Hookdeck in front, Justt only ever sees Hookdeck's response, and your own errors are retried by Hookdeck.
Secret format and sample mismatches
The Problem: Verification code copied from the docs doesn't match production traffic.
Why It Happens: The secret's prefix isn't stated, and the signature sample uses an event_type body that real events don't.
Workarounds:
- Confirm the
whsec_prefix in the dashboard, and route ontype.
How Hookdeck Can Help: Hookdeck's request log shows each Justt delivery exactly as sent, which settles questions the docs leave open.
Best practices
Keep the raw body intact
Use express.raw({ type: '*/*' }), await request.text() in Next.js route handlers, or FastAPI's await request.body(), and disable CSRF protection on the route.
Order by timestamp
Chargeback updates can be retried and arrive late. Keep the latest timestamp per chargebackId and skip older updates.
Mind dueDate
Chargeback events include the response deadline. If you track disputes outside Justt, alert on approaching dueDate values rather than waiting for the next event.
Deduplicate on webhook-id
Store it with each processed event; the idempotency guide shows how.
Conclusion
Justt's webhooks give you a detailed feed of disputes, alerts and the refunds and cancellations Justt performs for you. Verify with the endpoint's whsec_ secret, answer within 15 seconds, route the failure events to people, never return 410 by accident, and code defensively where the docs disagree.
Hookdeck Event Gateway can take that delivery burden off your service, verifying each event and routing failures separately from routine updates. Try it for free with a Justt source.