Webhook management
Configure multiple named webhooks per merchant, each scoped to a mode and a set of event types, and inspect, resend, or test deliveries
Kashier's webhook management module lets you configure multiple named webhooks per merchant instead of a single fixed URL. Each webhook is scoped to a mode (test or live) and subscribed to a specific set of event types, so different endpoints can receive different slices of your traffic — for example, a production endpoint that only cares about refund and transfer status changes, and a separate test endpoint your staging environment listens to.
This page covers creating and managing webhooks, looking up delivery records for a transaction or transfer, resending a delivery, and sending a test payload to verify an endpoint is reachable. For the shape of the event payload itself and signature verification, see Webhook payloads and the Webhooks guide.
All endpoints on this page are merchant-authenticated — send your secret key in the Authorization header, the same as everywhere else in the API.
The delivery-records endpoints need a permission too
Reading delivery records requires the webhook.view_webhook permission, and resending a delivery requires webhook.resend_webhook. These are dedicated webhook permissions rather than a side effect of your transaction or transfer access, and they're enforced for merchant users, not only for Kashier staff — a request without them is rejected with 403 even when the key itself is valid. Permission changes are cached for roughly 24 hours, so a newly granted permission may not take effect straight away. The CRUD endpoints (list, create, update, delete) are merchant-gated too, but which privilege they check wasn't pinned down at design time — if one returns 403 on a valid key, ask your Kashier contact which permission your user is missing.
If you used the old single-URL webhook
If your account was set up with Kashier's older single stored webhook URL, there's nothing to re-create. That URL is migrated into webhook management for you, so List your webhooks returns it as a webhook named Legacy webhook, subscribed to every event type, with isActive carrying over whether the old webhook was enabled and mode derived from your account's current mode. It keeps receiving exactly what it received before the migration.
From there it's an ordinary webhook. Rename it, narrow its events to the ones you actually handle, add custom headers, or switch it off with Update a webhook. If you want separate test and live destinations, create a second webhook for the other mode rather than flipping this one's mode back and forth.
List your webhooks
Returns every webhook configured for your merchant, across both modes.
| Endpoint | Value |
|---|---|
| TEST-URL | https://test-api.kashier.io/v2/webhooks |
| LIVE-URL | https://api.kashier.io/v2/webhooks |
| Method | GET |
Full parameter and response reference → List your webhooks.
curl --location 'https://test-api.kashier.io/v2/webhooks' \
--header 'Authorization: YOUR_TEST_SECRET_KEY'Headers
| Key | Description |
|---|---|
| Authorization | The Authorization is a secret key that is used to identify the merchant. You can obtain it from Kashier's dashboard. Learn more about Authorization. |
Response
[
{
"_id": "66a1f2c3e4b0a1234567890a",
"merchantId": "MID-10293",
"name": "Orders production endpoint",
"url": "https://shop.example.com/kashier/webhook",
"headers": {
"X-Shop-Token": "whsec_live_abc123",
"X-Env": "production"
},
"events": ["pay", "refund", "void", "TRANSFERRED"],
"mode": "live",
"isActive": true,
"createdAt": "2026-06-22T12:00:00.000Z",
"updatedAt": "2026-06-22T12:00:00.000Z"
}
]| Field | Description |
|---|---|
_id | Unique identifier for the webhook — this is the webhookId used in Update a webhook and Delete a webhook. |
merchantId | The merchant the webhook belongs to. |
name | Your label for the webhook. |
url | The endpoint Kashier delivers to. |
headers | Custom headers Kashier sends with every delivery to this webhook, as a flat string-to-string map. |
events | The event types this webhook is subscribed to — see Create a webhook for the full list. |
mode | test or live — the webhook only receives events emitted in this mode. |
isActive | Whether the webhook currently receives deliveries. |
createdAt / updatedAt | Timestamps. |
Create a webhook
Creates a new webhook for your merchant.
| Endpoint | Value |
|---|---|
| TEST-URL | https://test-api.kashier.io/v2/webhooks |
| LIVE-URL | https://api.kashier.io/v2/webhooks |
| Method | POST |
Full parameter and response reference → Create a webhook.
Body parameters
| Key | Description |
|---|---|
| name | Your label for the webhook. |
| url | The endpoint Kashier delivers to. Must be a public https URL — non-https schemes, localhost, loopback, link-local, and private (RFC-1918) addresses are rejected. |
| headers | Optional. Custom headers to send with every delivery to this webhook, as a flat string-to-string map. Defaults to {}. |
| events | Array of event types to subscribe this webhook to — see the table below. |
| mode | test or live. The webhook only receives events emitted in this mode. |
| isActive | Optional. Whether the webhook is enabled. Defaults to true. |
Max 5 webhooks per mode
You can configure up to 5 webhooks per mode (5 test + 5 live = 10 total). Creating a 6th webhook for a mode that's already at the limit is rejected. This limit is taken from Kashier's webhook-management design documentation rather than a live API response, and is flagged there as still-to-confirm (per-mode vs. global was open at design time). If a 6th webhook is unexpectedly accepted or rejected, treat your account's actual behavior as authoritative.
events accepts any of the following values:
| Category | Values |
|---|---|
| Transaction operations | pay, authorize, capture, refund, void, reversal |
| Transfer statuses | INITIATED, IN_TRANSIT, TRANSFERRED, FAILED |
The mixed casing is deliberate — each value matches the producing service's own literal, so transaction operations are lowercase and transfer statuses uppercase. Send them exactly as written.
Subscribing to refund does not cover partial refunds
The subscribable list holds base operations only, and matching is exact. A transaction whose operation is partial_refund does not match a refund subscription, so a webhook subscribed to refund won't be delivered that transaction. A per-request destination is not event-filtered and still receives it. The same applies to any other operation outside the list above.
Not every transfer status is subscribable
The subscribable list is narrower than the full transfer lifecycle. PENDING (and, for batches, PARTIALLY_TRANSFERRED) appear in the payout status values but aren't subscribable here, so a transfer entering one of them won't trigger a delivery. If you need a status that isn't listed, use the create-webhook "Try it" panel below to confirm the accepted set against your own account before assuming it's unavailable.
One webhook, two signing keys
Transaction events and transfer events are signed with different keys and different serializations, so a webhook subscribed to both — like the example below — needs two verifiers and has to pick one based on the event it received.
- Transaction events are signed with your Payment API Key for the webhook's mode: your test key signs
test-mode deliveries, your live key signsliveones. The signed string sortssignatureKeysalphabetically and URL-encodes each value. See signature verification. - Transfer events are signed with your Transfer API Key, over that payload's own
signatureKeysin array order with raw, un-encoded values. See Payout webhooks.
Reusing one verifier for both produces the wrong hash for one of them.
curl --location 'https://test-api.kashier.io/v2/webhooks' \
--header 'Authorization: YOUR_TEST_SECRET_KEY' \
--header 'Content-Type: application/json' \
--data '{
"name": "Orders staging endpoint",
"url": "https://your-website.com/kashier-webhook",
"headers": {
"X-Shop-Token": "replace-with-your-own-token",
"X-Env": "staging"
},
"events": ["pay", "refund", "void", "TRANSFERRED"],
"mode": "test",
"isActive": true
}'For a production webhook, send the same body with "mode": "live" to https://api.kashier.io/v2/webhooks using your live secret key — keep mode and the host you call in step rather than creating a live webhook from the test host.
This panel creates a real webhook
Unlike the read-only panels on this page, sending this one leaves persistent state on the account whose keys you saved: a webhook that stays there until you delete it, and that counts against the 5-per-mode limit above. The body below is deliberately defanged — mode is test, isActive is false, and the URL is a placeholder — so nothing is ever delivered anywhere. Delete it afterwards with Delete a webhook, using the _id from the response.
Headers
| Key | Description |
|---|---|
| Authorization | The Authorization is a secret key that is used to identify the merchant. You can obtain it from Kashier's dashboard. Learn more about Authorization. |
Response
Returns the created webhook, in the same shape as an entry in List your webhooks.
Update a webhook
Updates an existing webhook. Send only the fields you want to change.
| Endpoint | Value |
|---|---|
| TEST-URL | https://test-api.kashier.io/v2/webhooks/:webhookId |
| LIVE-URL | https://api.kashier.io/v2/webhooks/:webhookId |
| Method | PUT |
Full parameter and response reference → Update a webhook.
Body parameters
Same fields as Create a webhook — name, url, headers, events, mode, isActive — all optional on update. The https-only URL rule applies here too. Changing mode re-checks the 5-per-mode limit against the target mode.
curl --location --request PUT 'https://test-api.kashier.io/v2/webhooks/:webhookId' \
--header 'Authorization: YOUR_TEST_SECRET_KEY' \
--header 'Content-Type: application/json' \
--data '{
"isActive": false
}'Headers
| Key | Description |
|---|---|
| Authorization | The Authorization is a secret key that is used to identify the merchant. You can obtain it from Kashier's dashboard. Learn more about Authorization. |
Response
Returns the updated webhook, in the same shape as an entry in List your webhooks.
Delete a webhook
Removes a webhook. There's no undo — deliveries already recorded against it stay visible in delivery records.
| Endpoint | Value |
|---|---|
| TEST-URL | https://test-api.kashier.io/v2/webhooks/:webhookId |
| LIVE-URL | https://api.kashier.io/v2/webhooks/:webhookId |
| Method | DELETE |
Full parameter and response reference → Delete a webhook.
curl --location --request DELETE 'https://test-api.kashier.io/v2/webhooks/:webhookId' \
--header 'Authorization: YOUR_TEST_SECRET_KEY'Headers
| Key | Description |
|---|---|
| Authorization | The Authorization is a secret key that is used to identify the merchant. You can obtain it from Kashier's dashboard. Learn more about Authorization. |
Get delivery records for a transaction
Returns the delivery log for a given transaction — every webhook attempt made for it, across all your configured webhooks, plus any per-session webhook you passed directly on that transaction.
| Endpoint | Value |
|---|---|
| TEST-URL | https://test-api.kashier.io/v2/webhooks/records/transaction/:transactionId |
| LIVE-URL | https://api.kashier.io/v2/webhooks/records/transaction/:transactionId |
| Method | GET |
Full parameter and response reference → Get delivery records for a transaction.
curl --location 'https://test-api.kashier.io/v2/webhooks/records/transaction/:transactionId' \
--header 'Authorization: YOUR_TEST_SECRET_KEY'Not live on the test environment yet
This route isn't registered on test-api.kashier.io today — calling it there returns a 404 rather than a records payload, while the webhook CRUD endpoints on the same router answer normally. It's documented here because it's designed and ticketed, but there's no "Try it" panel for it until it ships. Check with your Kashier contact before building against it.
Headers
| Key | Description |
|---|---|
| Authorization | The Authorization is a secret key that is used to identify the merchant. You can obtain it from Kashier's dashboard. Learn more about Authorization. |
Response
{
"success": true,
"body": [
{
"resourceType": "transaction",
"resourceId": "TX-249893963",
"webhookId": "66a1f2c3e4b0a1234567890a",
"eventType": "pay",
"mode": "live",
"url": "https://shop.example.com/kashier/webhook",
"status": "delivered",
"attempts": 1,
"payload": { "...": "allow-listed fields only" },
"responses": [
{ "status": 200, "body": "OK", "date": "2026-06-22T12:00:01.000Z" }
],
"isServerWebhook": false,
"isTest": false,
"createdAt": "2026-06-22T12:00:00.000Z",
"updatedAt": "2026-06-22T12:00:01.000Z"
}
]
}| Field | Description |
|---|---|
resourceType | transaction for records returned here. |
resourceId | The transaction ID this delivery relates to. |
webhookId | The webhook this delivery came from, or null if it was a per-session webhook passed directly on the transaction. |
eventType | The event that triggered this delivery. |
mode | test or live. |
url | The destination URL the delivery was sent to. |
status | Delivery status — pending, delivered, retrying, or failed. A record sits in retrying between backoff attempts and only reaches failed once the attempt cap is exhausted. |
attempts | Number of delivery attempts recorded. |
payload | The request body sent, projected to a fixed, always-visible field set. Custom headers you configured are never included in this response. |
responses | One entry per attempt: the receiving endpoint's status, body, and date. |
isServerWebhook | true if this delivery went to a per-session webhook rather than a configured webhook. |
isTest | true if this record came from Test a webhook rather than a real delivery. |
Get delivery records for a transfer
Same records lookup as above, scoped to a transfer instead of a transaction.
| Endpoint | Value |
|---|---|
| TEST-URL | https://test-api.kashier.io/v2/webhooks/records/transfer/:transferId |
| LIVE-URL | https://api.kashier.io/v2/webhooks/records/transfer/:transferId |
| Method | GET |
Full parameter and response reference → Get delivery records for a transfer.
curl --location 'https://test-api.kashier.io/v2/webhooks/records/transfer/:transferId' \
--header 'Authorization: YOUR_TEST_SECRET_KEY'Not live on the test environment yet
Like its transaction counterpart, this route isn't registered on test-api.kashier.io today — calling it there returns a 404. It's documented because it's designed and ticketed, but there's no "Try it" panel for it until it ships.
Headers
| Key | Description |
|---|---|
| Authorization | The Authorization is a secret key that is used to identify the merchant. You can obtain it from Kashier's dashboard. Learn more about Authorization. |
Response
Same shape as Get delivery records for a transaction, except resourceType is transfer, resourceId is the transfer ID, and eventType is one of the transfer status values (INITIATED, IN_TRANSIT, TRANSFERRED, FAILED).
Resend a delivery
Re-sends a previously recorded delivery — the original stored request, byte-for-byte, including its original signature — to the same URL. Useful after fixing an endpoint that was down or returning errors. The outcome is written to a new record; the original record is never modified, so your delivery history stays intact.
A record can be resent from any status, including delivered. The endpoint takes no request body — everything it needs comes from recordId in the path.
| Endpoint | Value |
|---|---|
| TEST-URL | https://test-api.kashier.io/v2/webhooks/records/:recordId/resend |
| LIVE-URL | https://api.kashier.io/v2/webhooks/records/:recordId/resend |
| Method | POST |
Full parameter and response reference → Resend a delivery.
30-second cooldown
Resending the same record again within 30 seconds returns 429. Wait for the cooldown to clear before retrying.
curl --location --request POST 'https://test-api.kashier.io/v2/webhooks/records/:recordId/resend' \
--header 'Authorization: YOUR_TEST_SECRET_KEY'Headers
| Key | Description |
|---|---|
| Authorization | The Authorization is a secret key that is used to identify the merchant. You can obtain it from Kashier's dashboard. Learn more about Authorization. |
Response
{
"recordId": "66a20a1ce4b0a1234567891b",
"clonedFromRecordId": "66a1f2c3e4b0a1234567890c",
"status": "delivered",
"attempts": 1,
"lastResponseStatus": 200
}| Field | Description |
|---|---|
recordId | ID of the new delivery record created for this resend. |
clonedFromRecordId | ID of the original record this resend replayed. |
status | Outcome of the resend, e.g. delivered or failed. |
attempts | Attempts recorded on the new record (always 1 immediately after a resend). |
lastResponseStatus | HTTP status your endpoint returned for the resend attempt. |
Test a webhook
Sends a signed sample payload to a webhook's configured URL so you can confirm the endpoint is reachable and your signature verification works — without waiting for a real event. The test delivery is recorded like any other delivery, but flagged isTest: true so it's distinguishable in your delivery history.
| Endpoint | Value |
|---|---|
| TEST-URL | https://test-api.kashier.io/v2/webhooks/:id/test |
| LIVE-URL | https://api.kashier.io/v2/webhooks/:id/test |
| Method | POST |
Full parameter and response reference → Test a webhook.
curl --location --request POST 'https://test-api.kashier.io/v2/webhooks/:id/test' \
--header 'Authorization: YOUR_TEST_SECRET_KEY'Not live on the test environment yet
This route isn't registered on test-api.kashier.io today — calling it there returns a 404, so there's no "Try it" panel for it. Until it ships, verify an endpoint by running a real test-mode payment against it and reading the outcome from your own server logs.
Headers
| Key | Description |
|---|---|
| Authorization | The Authorization is a secret key that is used to identify the merchant. You can obtain it from Kashier's dashboard. Learn more about Authorization. |
Response
{
"success": true,
"statusCode": 200,
"responseBody": "OK",
"latencyMs": 184
}| Field | Description |
|---|---|
success | true if your endpoint returned a 2xx status. |
statusCode | HTTP status your endpoint returned. |
responseBody | A truncated snippet of your endpoint's response body. |
latencyMs | Round-trip time for the test delivery, in milliseconds. |