Gareth Wilson Gareth Wilson

Guide to Justt Webhooks: Features and Best Practices

Published


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

FeatureDetails
AvailabilityNot yet enabled for every customer
ConfigurationAccount level view > Webhooks tab
Subscription modelSelected types, or all types when none are selected
SignatureStandard Webhooks HMAC-SHA256
Headerswebhook-id (msg_...), webhook-timestamp, webhook-signature
SecretSigning Secret per endpoint (prefix not printed; the sample implies whsec_)
Body{type, timestamp, data}
Event routingBody field type
Response window15 seconds (the API reference says 30)
Retries8 attempts over about 27.6 hours (the API reference says up to 3)
Permanent failure410 Gone stops retries
DisablingAfter 5 consecutive days of failures
RecoveryResend one message, or recover failures since a date
TransformationsCan rewrite payloads or routing before delivery
Source IPsNot published

Events and payloads

FieldMeaning
typeThe event type, for example chargeback.created. This is what handlers switch on
timestampISO 8601 time the event occurred
dataEvent-specific object (see below)

Common Event Types

EventTriggered WhenCommon Use Cases
chargeback.createdA new chargeback is received from any connected PSPOpen a case, alert the team
chargeback.updatedA chargeback is updatedRefresh your copy of the record
chargeback.status.updatedA chargeback status update is receivedTrack the dispute lifecycle
chargeback.evidence.submittedEvidence is submitted to the PSP; data.status carries the outcomeConfirm the dispute response, retry on failed
chargeback.acceptedA chargeback acceptance is confirmed by the PSP; data.status carries the outcomeWrite off the amount
pre_chargeback_alert.receivedA pre-chargeback alert arrives from CDRN, RDR or EthocaDecide whether to refund
pre_chargeback_alert.refund.succeededThe transaction linked to a refunded alert was refunded through the PSPClose the alert
pre_chargeback_alert.refund.failedA refund alert could not be processedRefund manually
pre_chargeback_alert.transaction_matchedAn inbound alert was matched to its transaction. Opt-in, enabled by Justt per accountLink the alert to your order
subscription.cancel_immediately.successA CRM subscription (for example Chargebee or Recurly) was cancelled after a refundUpdate entitlements
subscription.cancel_immediately.failedThe cancellation failed (subscription not found, already cancelled). Justt flags the alert as Action RequiredCancel by hand

Justt calls these its supported core event types, so treat the list as a minimum.

data by event family

EventsNotable data fields
chargeback.created, chargeback.updated, chargeback.status.updatedThe full chargeback: id, chargebackId, referenceAccountId, psp, currency, amount, status, pspStatus, lifecycleStage, reasonCode, reason, reasonGroup, cardScheme, dueDate, winProbability and more
chargeback.evidence.submittedsubmitEvidenceId, fileId, id, chargebackId, status (succeeded or failed), submittedAt, error
chargeback.acceptedid, chargebackId, status (succeeded or failed), acceptedAt, error
pre_chargeback_alert.received, pre_chargeback_alert.transaction_matchedinternalAlertId, provider (CDRN, RDR or Ethoca), providerAlertId, alertType, alertTimestamp, arn, transaction; matched events add matchedTransaction
pre_chargeback_alert.refund.succeeded, pre_chargeback_alert.refund.failedinternalAlertId, providerName, providerAlertId, psp (pspName, transactionId, plus refundId / refundedDate or refundError), amount, currency
subscription.cancel_immediately.success, subscription.cancel_immediately.failedsubscriptionId, 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

  1. In the Justt app, switch to the Account level view and open Webhooks (https://app.justt.ai/aggregated/webhooks).
  2. Add an endpoint with a public HTTPS URL that accepts POST and replies within 15 seconds.
  3. Select event types. Selecting none subscribes to everything, which is useful while testing; in production choose the types you handle.
  4. 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_chargeback_alert.refund.failed and subscription.cancel_immediately.failed 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 410 from the webhook route; use 404 or 503 for 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 on type.

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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.