Webhooks

Manage Weenx webhook endpoints, verify HMAC-SHA256 signatures against the raw body, handle deliveries and retries, and consume the full outbound event catalog.

Webhooks push payment, settlement, and payout state to your backend as it happens, so you do not have to poll. Use them for fast notification, then reconcile with the read endpoints when you need authoritative current state.

Every webhook is signed. Verify the signature against the raw request body before you parse or act on the payload. Creating and managing webhook endpoints requires the webhooks:read / webhooks:write scopes, and creating an endpoint additionally requires your plan's webhooks capability.

Managing endpoints

Create an endpoint

curl --request POST \
  --url https://api.weenx.com/v1/webhooks \
  --header 'Content-Type: application/json' \
  --header 'X-Api-Key: YOUR_WORKSPACE_API_KEY' \
  --data '{
    "name": "Production events",
    "url": "https://example.com/weenx/webhooks",
    "events": ["PaymentConfirmed", "PayoutCompleted"]
  }'

The request body carries:

  • name — required
  • url — required, an absolute HTTPS URI that resolves to a public address. URLs that are not HTTPS, resolve to a private or loopback address, or embed credentials are rejected when the endpoint is created, and the check is re-run before each delivery
  • events — optional array of event names, joined into the subscription mask
  • eventsMask — optional explicit mask; use * to subscribe to every event

If neither events nor eventsMask is supplied, the endpoint subscribes to all events (*). The response returns:

  • webhookEndpointId
  • signingSecretshown once; store it now to verify signatures
  • eventsMask — the effective subscription mask

The signing secret is not shown again. If you lose it, rotate the secret.

List, inspect, and disable

  • GET /v1/webhooks — list your endpoints (id, name, url, eventsMask, isActive, secretVersion, timestamps)
  • GET /v1/webhooks/{id}/deliveries — paged delivery history for one endpoint (page, pageSize up to 200), each row carrying attemptNo, status, responseStatus, and nextRetryAtUtc
  • POST /v1/webhooks/{id}/rotate-secret — rotate the signing secret; the new secret is returned once, and the previous secret stays valid through a short grace period so you can roll over without dropping deliveries
  • POST /v1/webhooks/{id}/test — dispatch a sample delivery (optional sampleEventType, default PaymentConfirmed) to exercise your verification path
  • POST /v1/webhooks/{id}/disable — deactivate an endpoint; the dispatcher only delivers to active endpoints
  • GET /v1/webhooks/events — the static event catalog (name, category, description, and field list per event)

Delivery headers

Every delivery is an HTTP POST with a Content-Type: application/json body and exactly three Weenx headers:

HeaderContents
X-Weenx-Eventthe event name, e.g. PaymentConfirmed
X-Weenx-Timestampthe Unix epoch seconds at which the delivery was signed
X-Weenx-Signaturelowercase hex of HMAC_SHA256(secret, "{timestamp}.{rawBody}")

There is no separate delivery-id header. Deduplicate on a natural identifier inside the event payload — paymentId, payoutRequestId, or batchId, combined with the event name — because a retried delivery repeats the same payload.

Signature verification

Weenx signs each delivery with HMAC-SHA256. The signed message is the exact string {timestamp}.{rawBody}, where timestamp is the value of the X-Weenx-Timestamp header and rawBody is the request body exactly as received — do not re-serialize parsed JSON before verifying.

One detail matters: the signingSecret you receive on create/rotate is a 64-character lowercase hex string, and the HMAC is keyed with the 32 raw bytes that hex encodes. Hex-decode the secret before computing the HMAC.

A production consumer should:

  1. read the raw request body exactly as delivered
  2. read X-Weenx-Signature and X-Weenx-Timestamp
  3. reject deliveries whose timestamp is outside your freshness window (a tolerance of 300 seconds is a sensible default) to mitigate replay
  4. compute HMAC_SHA256(hexDecode(secret), "{timestamp}.{rawBody}") and hex-encode it
  5. compare it to X-Weenx-Signature in constant time (an optional sha256= prefix and any casing are tolerated)
  6. reject on mismatch; only then parse and process the payload
  7. process business effects idempotently, keyed on the payload identifier

Using the SDK

The official @weenx/integration-sdk package ships a verifier that matches the server byte-for-byte. Mount a raw-body handler so the bytes are unmodified:

import express from "express";
import { verifyWebhookSignature } from "@weenx/integration-sdk";

const app = express();

app.post("/weenx/webhooks", express.raw({ type: "*/*" }), async (req, res) => {
  const ok = await verifyWebhookSignature({
    payload: req.body, // raw bytes — do NOT use express.json() on this route
    signature: req.header("X-Weenx-Signature"),
    timestamp: req.header("X-Weenx-Timestamp"),
    secret: process.env.WEENX_WEBHOOK_SECRET!,
  });
  if (!ok) return res.status(400).end();

  const event = req.header("X-Weenx-Event");
  // ...handle the event idempotently...
  res.status(200).end();
});

Verifying manually

Any language with an HMAC library can verify a delivery. In Node without the SDK:

const crypto = require("crypto");

function isValid(rawBody, timestamp, signature, hexSecret) {
  const key = Buffer.from(hexSecret, "hex");
  const expected = crypto
    .createHmac("sha256", key)
    .update(`${timestamp}.${rawBody}`, "utf8")
    .digest("hex");
  const given = signature.replace(/^sha256=/i, "").toLowerCase();
  return (
    expected.length === given.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(given))
  );
}

Deliveries and retries

Return 2xx quickly to acknowledge a delivery. A delivery that fails — a non-2xx response, a timeout, or a connection error — is retried on a short cadence (roughly once a minute), and each attempt is recorded with its attemptNo, status, responseStatus, and the nextRetryAtUtc for the next attempt. A successful delivery clears nextRetryAtUtc. Inspect the history at GET /v1/webhooks/{id}/deliveries.

Recommended consumer architecture:

  1. accept the HTTPS request
  2. verify the signature against the raw body
  3. deduplicate on the payload identifier
  4. persist the raw payload and headers
  5. return 2xx after safe acceptance
  6. process the event asynchronously and idempotently
  7. reconcile with the read endpoints where you need the latest state

Event catalog

GET /v1/webhooks/events returns the field-level catalog for your integration; read it from there rather than hard-coding this list. Each entry carries a name, a category, a description, and a list of payload fields (each with name, type, required, and description), covering the Payments, Invoices, Settlements, and Payouts events below. The catalog is the same for every workspace — it states what Weenx can emit, not what your endpoint is currently subscribed to.

Payments

EventWhenKey payload fields
PaymentDetectedAn incoming payment is detected (pre-confirmation).paymentId, invoiceId (nullable), amount, currencyNetworkId
PaymentConfirmedAn incoming payment reaches finality.paymentId, invoiceId (nullable), confirmationCount

Invoices

EventWhenKey payload fields
InvoiceExpiredAn invoice reaches its expiry without being paid.invoiceId, externalOrderId, expiredAtUtc

Settlements

EventWhenKey payload fields
SettlementCompletedA settlement batch completes successfully.batchId, settledAmount, txHash
SettlementPartiallyCompletedA settlement batch partially completes.batchId, successfulItemCount, failedItemCount
SettlementFailedA settlement batch fails.batchId, failureReason

Payouts

EventWhenKey payload fields
PayoutSentA payout transaction is broadcast.payoutRequestId, txHash
PayoutConfirmingA payout enters the confirming lifecycle.payoutRequestId, confirmationCount
PayoutCompletedA payout completes.payoutRequestId, completedAtUtc
PayoutFailedA payout fails.payoutRequestId, failureReason
PayoutCancelledA payout is cancelled.payoutRequestId, reason
PayoutRejectedA payout is rejected.payoutRequestId, reason
PayoutBatchCompletedA payout batch completes.batchId, successfulItemCount
PayoutBatchPartiallyCompletedA payout batch partially completes.batchId, failedItemCount
PayoutBatchFailedA payout batch fails.batchId, failureReason

If your endpoint filters events, keep the filter aligned with the catalog returned by GET /v1/webhooks/events so events your integration needs are not silently dropped. The critical events to subscribe to for most integrations are PaymentConfirmed, InvoiceExpired, and the Payout* lifecycle events.

Reconciliation

Webhooks are for fast notification; the read endpoints are authoritative. After you accept a delivery, reconcile through:

  • GET /v1/invoices/{invoiceId}/status — invoice state
  • GET /v1/payments, GET /v1/payments/deposits, GET /v1/payments/settlement-batches — on-chain payment, deposit, and settlement detail
  • GET /v1/payouts/{payoutRequestId} — payout state
  • GET /v1/balances — current wallet balances

Did this page help you?