Solvador
Dashboard & Billing

Webhooks

Receive a signed HTTP callback on every settlement. Subscribe to settlement.succeeded and settlement.failed, verify the HMAC-SHA256 signature, and reconcile payments without polling.

Webhooks push a signed HTTP request to your server every time a settlement runs, so you do not have to poll /settle results or the dashboard. Each settlement that is attributed to one of your API keys can notify one or more endpoints you configure, and success and failure are separate events you subscribe to independently.

A webhook is the recommended way to fulfill orders, credit balances, send receipts, or update your own database the moment a payment settles onchain.

Creating an endpoint

  1. Sign in at dashboard.solvador.com with Google or GitHub.
  2. Open the Webhooks tab.
  3. Click Add endpoint, enter the HTTPS URL that will receive deliveries, and choose which events to subscribe to.
  4. Copy the signing secret shown after creation. You will use it to verify that every delivery genuinely came from Solvador.

You can register several endpoints. Each one has its own signing secret and its own set of subscribed events, so you can, for example, send successes to your fulfillment service and failures to an alerting service.

The signing secret authenticates every delivery. Treat it like a password: keep it server-side, never commit it to source control, and never expose it in client code. You can re-reveal or roll the secret at any time from the endpoint’s actions in the dashboard.

Events

You subscribe each endpoint to one or both of these event types. One settlement produces exactly one event.

EventWhen it fires
settlement.succeededA settlement completed onchain (success: true from /settle).
settlement.failedA settlement was processed but did not succeed (success: false, for example an invalid signature or insufficient funds).

A settlement that throws a transport or internal error (an HTTP 500 from /settle) does not produce an event, because no settlement result was recorded. Only processed settlements, successful or failed, are delivered.

For batched settlement schemes such as batch-settlement, each underlying payment in the batch produces its own event. Several events can therefore arrive close together, and they will share the same onchain txHash.

Event payload

Every delivery is a JSON POST with an envelope that wraps the settlement data. A settlement.succeeded body looks like this:

{
  "id": "evt_9f3c8b2a-1d4e-4a77-9b1c-2c0f7e5a1b3d",
  "type": "settlement.succeeded",
  "created": "2026-07-21T12:34:56.000Z",
  "data": {
    "id": "9f3c8b2a-1d4e-4a77-9b1c-2c0f7e5a1b3d",
    "network": "eip155:8453",
    "scheme": "exact",
    "payer": "0xPayerAddress",
    "payee": "0xYourReceivingAddress",
    "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    "amount": "10000",
    "txHash": "0x6fe1…c40b",
    "status": "settled",
    "opType": null,
    "units": 1,
    "settledAt": "2026-07-21T12:34:56.000Z"
  }
}

A settlement.failed body carries the same envelope, with status set to failed and two extra fields inside data that explain the failure:

{
  "id": "evt_5a1b3d2c-…",
  "type": "settlement.failed",
  "created": "2026-07-21T12:35:10.000Z",
  "data": {
    "id": "5a1b3d2c-…",
    "network": "eip155:8453",
    "scheme": "exact",
    "payer": "0xPayerAddress",
    "payee": "0xYourReceivingAddress",
    "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    "amount": "10000",
    "txHash": "",
    "status": "failed",
    "opType": null,
    "units": 0,
    "settledAt": "2026-07-21T12:35:10.000Z",
    "errorReason": "invalid_signature",
    "errorMessage": "authorization signature does not match the payer"
  }
}

Envelope fields

FieldTypeDescription
idstringUnique event id, formatted evt_<transactionId>. Stable across retries and across endpoints, so use it to deduplicate.
typestringsettlement.succeeded or settlement.failed.
createdstringISO 8601 timestamp of the settlement.
dataobjectThe settlement, described below. Mirrors the row shown in your dashboard’s Settlements tab.

data fields

FieldTypeDescription
idstringThe settlement (transaction) id. Same value used to build the event id.
networkstringCAIP-2 network the settlement ran on, for example eip155:8453 or solana:5eykt4Us….
schemestringThe payment scheme: exact, upto, or batch-settlement.
payerstringThe paying address.
payeestringThe receiving address (payTo).
assetstringToken contract or mint address.
amountstringSettled amount in atomic units, as a string to stay big-integer safe.
txHashstringOnchain transaction hash. Shared across a batch, and empty when a failed settlement produced no transaction.
statusstringsettled or failed.
opTypestring?The batch operation type (deposit, claim, settle, refund) for batch-settlement, or null for exact and upto.
unitsnumberBillable payment units for this settlement.
settledAtstringISO 8601 timestamp, equal to the envelope created.
errorReasonstring?Machine-readable failure code. Present only on settlement.failed.
errorMessagestring?Human-readable failure detail. Present only on settlement.failed.

Delivery headers

Every delivery carries these headers:

HeaderDescription
Content-TypeAlways application/json.
Solvador-SignatureThe HMAC signature, formatted t=<unix>,v1=<hex>. See Verifying the signature.
Solvador-Event-IdThe envelope id (evt_…). Use it to deduplicate.
Solvador-Event-Typesettlement.succeeded or settlement.failed.
Solvador-Delivery-IdA per-attempt-series id (whd_…) for support and debugging.
User-AgentSolvador-Webhooks/1.0.

Verifying the signature

Every delivery is signed with HMAC-SHA256 using your endpoint’s signing secret. Verifying the signature proves the request came from Solvador and that the body was not altered in transit. Always verify before you act on a delivery.

The Solvador-Signature header has the form t=<timestamp>,v1=<signature>, where:

  • t is the unix timestamp (in seconds) when the request was signed.
  • v1 is the lowercase hex HMAC-SHA256 of the timestamp, a literal dot, and the raw body joined together, that is t + "." + rawBody, keyed with your signing secret. rawBody is the raw request body bytes, exactly as received.

To verify:

  1. Read the raw request body as a string, before any JSON parsing or re-serialization.
  2. Read t and every v1 value from the Solvador-Signature header.
  3. Compute HMAC_SHA256(secret, t + "." + rawBody) and hex-encode it.
  4. Compare it against each v1 in constant time. Accept if any matches.
  5. Optionally reject deliveries whose t is more than a few minutes old, to bound replay.

Sign and verify the raw request body, not a parsed then re-serialized object. Re-serializing can reorder keys or change whitespace, which changes the bytes and breaks the signature. In Express, capture the raw body (for example with express.raw({ type: "application/json" })) on the webhook route.

A Node.js example, framework-agnostic aside from how you obtain the raw body:

const crypto = require("crypto");

// rawBody: the exact request body string
// header:  the value of the Solvador-Signature header
// secret:  your endpoint's signing secret (whsec_...)
function verifySolvadorWebhook(rawBody, header, secret) {
  const parts = header.split(",").map((p) => p.trim());
  const t = parts.find((p) => p.startsWith("t="))?.slice(2);
  const signatures = parts.filter((p) => p.startsWith("v1=")).map((p) => p.slice(3));
  if (!t || signatures.length === 0) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.${rawBody}`, "utf8")
    .digest("hex");

  // A roll can send old and new signatures during a grace window: accept any match.
  const signatureOk = signatures.some(
    (sig) =>
      sig.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)),
  );
  if (!signatureOk) return false;

  // Optional replay window: reject if older than 5 minutes.
  const ageSeconds = Math.abs(Date.now() / 1000 - Number(t));
  return ageSeconds <= 300;
}

An Express handler that uses it:

const express = require("express");
const app = express();

app.post(
  "/webhooks/solvador",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const rawBody = req.body.toString("utf8");
    const ok = verifySolvadorWebhook(
      rawBody,
      req.header("Solvador-Signature") ?? "",
      process.env.SOLVADOR_WEBHOOK_SECRET,
    );
    if (!ok) return res.status(400).send("invalid signature");

    const event = JSON.parse(rawBody);
    // Deduplicate on event.id, then handle the event.
    // Respond fast; do slow work asynchronously.
    res.status(200).send("ok");
  },
);

Responding to a delivery

Return any 2xx status to acknowledge a delivery. Anything else, including 3xx redirects, counts as a failure and is retried.

  • Respond within 10 seconds. Deliveries time out after that and are treated as failed.
  • Respond first, then do slow work asynchronously. Do not run long database writes or downstream calls before you acknowledge.
  • The response body is ignored. Only the status code matters.

Retries and failure handling

If a delivery does not return 2xx, Solvador retries it with an increasing backoff, up to 8 attempts spread over roughly 24 hours:

AttemptSent after the previous failure
1immediately
210 seconds
31 minute
45 minutes
530 minutes
62 hours
76 hours
812 hours

After the eighth attempt fails, the delivery is marked failed and is not retried again. An endpoint that keeps failing for a long streak is automatically disabled to protect your server and ours; a disabled endpoint stops receiving events until you re-enable it from the dashboard, which also clears its failure count.

Because a settlement never depends on webhook delivery, an outage on your side never affects payments. Deliveries are simply queued, retried, and, if they keep failing, recorded as failed in the delivery log.

Idempotency and ordering

Delivery is at-least-once, so the same event can arrive more than once (for example when your server returns 2xx after a network timeout already counted the attempt as failed).

  • Deduplicate on the event id (also sent as the Solvador-Event-Id header). It is stable across retries. Make your handler idempotent, so processing the same event twice is a no-op.
  • Do not rely on ordering. Concurrent delivery, independent retries, and batch fan-out mean events can arrive out of order. Key your state on the settlement, using txHash or the event id, rather than on arrival order.

Testing and the delivery log

The Webhooks tab gives you tools to develop and debug against real deliveries:

  • Send test delivers a synthetic settlement.succeeded event to a single endpoint, so you can confirm your receiver and signature verification work end to end before real traffic arrives.
  • Deliveries opens the delivery log for an endpoint: recent attempts with their status, the HTTP status you returned, and the attempt count.
  • Redeliver re-queues an existing delivery, which is useful after you fix a bug on your side.

During local development you can point an endpoint at a tunnel such as webhook.site or an HTTPS tunnel to your machine, then use Send test to inspect the exact headers and body.

Rotating the signing secret

You can reveal the current signing secret or roll it at any time from the endpoint’s actions.

Rolling generates a new secret and invalidates the old one. To rotate without downtime, roll in the dashboard, then update the secret in your environment and redeploy. During a roll’s grace window a delivery may be signed with both the old and the new secret, sending two v1 values in the Solvador-Signature header, so a verifier that accepts any matching v1 (as in the example above) keeps working across the change.

Security

  • HTTPS only. Webhook URLs must use https://. Plain http:// is rejected.
  • No private targets. URLs that resolve to loopback, link-local, or private address ranges (for example localhost, 127.0.0.1, 10.0.0.0/8, or the cloud metadata address 169.254.169.254) are rejected, and the address is re-checked at delivery time to prevent DNS rebinding.
  • Signing secrets are stored encrypted and are only ever revealed to you, the account owner, in the dashboard. Solvador signs each delivery on the server side, so the secret never leaves your account.
  • Keep your receiver’s secret server-side. Never verify signatures in client code.