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:
- Add your endpoint's address. It must use HTTPS.
- Choose which events it receives.
- 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
| Event | Sent when |
|---|---|
creator.created | A creator is added |
creator.updated | A creator's details or status change |
Invoices
| Event | Sent when |
|---|---|
invoice.submitted | An invoice is submitted, in the app, by email or through the API |
invoice.approved | An invoice is approved for payment |
invoice.rejected | An invoice is rejected |
invoice.paid | The payment for an invoice reaches the creator |
Payment runs
| Event | Sent when |
|---|---|
payment_run.prepared | A run is created and waiting for approval |
payment_run.approved | A person approves a run |
payment_run.withdrawn | A run is withdrawn before approval |
payment_run.funded | Your money for the run has arrived in full |
payment_run.completed | Every payment in the run has been made |
payment_run.partially_completed | Some payments in the run were made and some failed |
payment_run.failed | The run's payments failed |
Payments
| Event | Sent when |
|---|---|
payment.failed | A payment fails, including one returned by the creator's bank after it was sent |
Testing
| Event | Sent when |
|---|---|
test.ping | You 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
tis when StrideHQ sent it, as a Unix timestamp.v1is an HMAC SHA-256 of the timestamp, a full stop and the raw request body, using your endpoint's signing secret.
To verify:
- Take
tandv1from the header. - Join
t, a full stop and the raw request body, exactly as received. - Compute the HMAC SHA-256 of that, using your signing secret.
- Compare it with
v1using a constant-time comparison. - 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
idto skip ones you've already handled. - Events can arrive out of order. Don't assume
payment_run.fundedarrives afterpayment_run.approved. Compareupdated_aton the object, and ignore an event older than what you already have. - The object can change after the event.
data.objectis 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.