Webhooks

On this page

Webhooks tell your systems when something changes in StrideHQ, so you don't have to keep checking. When an event happens, StrideHQ sends an HTTPS POST to your endpoint with the details.

Set up an endpoint

In Settings > Developers > Webhooks, or through the API with a key that has the webhooks:manage scope:

  1. Add your endpoint's address. It must use HTTPS.
  2. Choose which events it receives.
  3. Copy the signing secret. It's shown once.

Sandbox and production endpoints are set up separately, and each endpoint has its own secret.

Events

Creators

EventSent when
creator.createdA creator is added
creator.updatedA creator's details or status change

Invoices

EventSent when
invoice.submittedAn invoice is submitted, in the app, by email or through the API
invoice.approvedAn invoice is approved for payment
invoice.rejectedAn invoice is rejected
invoice.paidThe payment for an invoice reaches the creator

Payment runs

EventSent when
payment_run.preparedA run is created and waiting for approval
payment_run.approvedA person approves a run
payment_run.withdrawnA run is withdrawn before approval
payment_run.fundedYour money for the run has arrived in full
payment_run.completedEvery payment in the run has been made
payment_run.partially_completedSome payments in the run were made and some failed
payment_run.failedThe run's payments failed

Payments

EventSent when
payment.failedA payment fails, including one returned by the creator's bank after it was sent

Testing

EventSent when
test.pingYou send a test event from Settings or the API

New event types may be added within v1. Ignore any you don't recognise.

What a delivery looks like

{
  "id": "evt_9a1c3e5b7d9f4b6a8c0e2a4c6e8a0b2d",
  "object": "event",
  "type": "payment_run.approved",
  "created_at": "2026-11-03T10:02:17Z",
  "api_version": "v1",
  "data": {
    "object": {
      "id": "run_3e5a7c9b1d2f4a6c8e0b2d4f6a8c0e1d",
      "object": "payment_run",
      "reference": "PR-STRIDE-26-053",
      "status": "awaiting_funds",
      "totals": [
        { "currency": "GBP", "amount": 1875749 }
      ]
    }
  }
}

data.object is the object as it is when the event is sent, in the same shape as the API returns it.

Respond quickly

Return any 2xx status within 10 seconds to confirm you've received it. Do any slow work afterwards, for example by adding the event to a queue.

Verify the signature

Every delivery is signed, so you can check it came from StrideHQ and wasn't changed. The signature is in the StrideHQ-Signature header:

StrideHQ-Signature: t=1793786537,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  • t is when StrideHQ sent it, as a Unix timestamp.
  • v1 is an HMAC SHA-256 of the timestamp, a full stop and the raw request body, using your endpoint's signing secret.

To verify:

  1. Take t and v1 from the header.
  2. Join t, a full stop and the raw request body, exactly as received.
  3. Compute the HMAC SHA-256 of that, using your signing secret.
  4. Compare it with v1 using a constant-time comparison.
  5. Reject the delivery if the timestamp is more than 5 minutes old.

Node.js:

import crypto from "node:crypto";

// rawBody must be the unparsed body as a string or Buffer.
// In Express: app.post("/webhooks/stridehq", express.raw({ type: "application/json" }), ...)
function verify(rawBody, header, secret) {
  if (!header) return false;
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (!parts.t || !parts.v1) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return (
    fresh &&
    expected.length === parts.v1.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
  );
}

Python:

import hashlib, hmac, time

def verify(raw_body: bytes, header: str | None, secret: str) -> bool:
    if not header:
        return False
    parts = dict(p.split("=", 1) for p in header.split(","))
    if "t" not in parts or "v1" not in parts:
        return False
    signed = f"{parts['t']}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    fresh = abs(time.time() - int(parts["t"])) < 300
    return fresh and hmac.compare_digest(expected, parts["v1"])

Use the raw body. If your framework parses the JSON first, the signature won't match.

Rotate a signing secret

In Settings > Developers > Webhooks, choose the endpoint and select Roll secret. The old secret keeps working for 24 hours, and during that time each delivery is signed with both, as two v1 values in the header. Accept the delivery if either matches.

Retries

If your endpoint doesn't return a 2xx, or doesn't respond in time, StrideHQ tries again after:

1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours, then 24 hours.

If every delivery to an endpoint has failed for 3 days, the endpoint is turned off and your Owner is emailed. Turn it back on in Settings > Developers > Webhooks once it's fixed.

Handle duplicates and order

  • The same event can arrive more than once. Use the event id to skip ones you've already handled.
  • Events can arrive out of order. Don't assume payment_run.funded arrives after payment_run.approved. Compare updated_at on the object, and ignore an event older than what you already have.
  • The object can change after the event. data.object is a snapshot. Before acting on money, such as marking an invoice paid in your accounting software, get the latest version from the API.

Delivery log

Settings > Developers > Webhooks shows every delivery for the last 30 days: the event, when it was sent, the status code your endpoint returned and each retry. You can resend any event from there.

Test your endpoint

In the sandbox, use the test helpers to move a payment run through each stage. Each step sends the same webhooks as in production.

Next steps

  • Flow of funds: what each payment run status means for your money.
  • API reference: every object that appears in data.object.
Updated 29 October 2026
Did this page help you?