CreditKit Pro webhooks
CreditKit Pro delivers 18 webhook event types to any public http or https endpoint. Each delivery is a JSON POST signed with HMAC-SHA256 and stamped with envelope version 2025-01-01. A failed delivery is retried up to four times on a fixed backoff ladder, and an endpoint that exhausts all five attempts is disabled automatically. Delivery runs in the background through Action Scheduler.
Webhooks are managed at Credit System → API & Webhooks, and over REST at /wp-json/pcs/v1/webhooks. The {id} segment of every webhook route is a UUID version 4 string of exactly 36 characters, matching the route pattern [a-f0-9\-]{36}.
The 18 event types
| Event | Fires when |
|---|---|
credit.added | Any addition to a balance: bundle purchase, admin grant or refund restoration. |
credit.spent | Any debit from a balance: credit-paid checkout or refund claw-back. |
credit.expired | The expiry job removes credits past their expiry date. |
bundle.purchased | A credit bundle order completes and the credits are granted. |
order.paid | An order is settled through the Pay with Credits gateway. |
refund.restored | An admin issues goodwill store credit from the order edit screen. |
fraud.flagged | The fraud engine scores a transaction at or above the high threshold. |
fraud.user_suspended | The fraud engine suspends a customer account. |
user.blocked | An admin or an automated rule blocks a customer. |
user.unblocked | A customer block is lifted. |
user.unsuspended | A suspended customer is reinstated, including by the cooldown job. |
user.login_locked | Login protection locks an account after repeated failures. |
ip.blocked | An IP address is added to the blocklist. |
ip.unblocked | An IP address block is lifted. |
balance.drift_detected | Reconciliation finds a balance that disagrees with the ledger. |
backup.failed | A scheduled credit-data backup fails. |
tier.downgraded | The CreditKit Pro licence tier for the site is downgraded. |
webhook.test | An admin presses Test on a webhook row. This event is hidden from the subscription form. |
Seventeen of those 18 are subscribable, because webhook.test is an internal ping CreditKit Pro sends on demand. tier.downgraded refers to the licence tier of the CreditKit Pro installation and carries no customer data. A nineteenth event, badge.earned, joins the registry only while the Rewards gamification toggle is on, and that toggle ships off.
Legacy event names from before 2.2.0 still work at subscribe time. CreditKit Pro resolves names such as user.suspension_cleared and security.fraud_alert to their canonical equivalents, then stores and delivers the canonical name only.
Envelope and headers
{
"api_version": "2025-01-01",
"event_id": "6f1c2b7e-6d1c-4d0a-9f4a-2a1a5f3f9c11",
"event": "credit.spent",
"webhook_id": "1b4e28ba-2fa1-11d2-883f-0016d3cca427",
"timestamp": 1774180800,
"data": {
"user_id": 42,
"amount": 12,
"new_balance": 88,
"description": "Order #1043",
"usage_type": "general",
"context": "order"
}
}
| Header | Value |
|---|---|
Content-Type | application/json |
X-PCS-Signature | sha256= followed by the HMAC hex digest of the raw body |
X-PCS-Event | The canonical event name, for example credit.spent |
X-PCS-Event-Id | UUID v4, identical across every retry of the same event |
X-PCS-API-Version | 2025-01-01 |
X-PCS-Attempt | Attempt number from 1 to 5 |
The event_id is generated once per triggering action and reused on every retry, so a receiver can deduplicate on it. Each request times out after 10 seconds.
Verifying the signature
CreditKit Pro shows the webhook secret once, at creation time, and stores only the SHA-256 hex digest of it. Signing uses that stored digest as the HMAC key, so a receiver hashes the secret first and uses the resulting 64-character lowercase hex string as the key.
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_PCS_SIGNATURE'] ?? '';
// $secret is the value shown once in wp-admin.
$key = hash('sha256', $secret);
$expected = 'sha256=' . hash_hmac('sha256', $payload, $key);
if (! hash_equals($expected, $signature)) {
http_response_code(401);
exit('Invalid signature');
}
Hash the raw request body. Re-encoding the JSON first changes key order and whitespace, which produces a different digest and a failed comparison.
Which event fires on which refund
CreditKit Pro has three refund mechanisms and each emits a different event. refund.restored covers only the manual goodwill path, so an integration listening for refund.restored alone misses the two automatic mechanisms.
| Refund situation | What happens to the balance | Event | Marker in data |
|---|---|---|---|
| An order paid with credits is refunded in wp-admin, over REST or by WP-CLI | Credits are returned in proportion to the refunded money amount | credit.added | type is refund |
| A money-paid order that granted credits from a bundle is refunded | Granted credits are clawed back in proportion, clamped at a zero balance by default | credit.spent | context is refund_clawback |
| An admin issues goodwill store credit from the order edit screen | Credits are added at the configured money-to-credits issuance rate | credit.added, then refund.restored | type is refund_credit on the first event |
The goodwill path emits two events for one admin action, so deduplicate on the transaction_id carried in the refund.restored payload.
Retries and auto-disable
A delivery counts as successful on an HTTP status from 200 to 299. Every other status, and every connection error, counts as a failure and schedules the next attempt in the Action Scheduler group pcs-webhooks.
| Attempt | Scheduled |
|---|---|
| 1 | Immediately, as an async job |
| 2 | 60 seconds after attempt 1 fails |
| 3 | 300 seconds after attempt 2 fails |
| 4 | 1,800 seconds after attempt 3 fails |
| 5 | 7,200 seconds after attempt 4 fails |
After attempt 5 fails, CreditKit Pro disables the webhook, records the reason on the row and writes a warning to the WooCommerce log source pcs-webhooks. Re-enabling does not replay the events missed while the endpoint was down.
The delivery log
CreditKit Pro keeps the last 20 delivery records per webhook and drops the oldest when the twenty-first arrives. Each record holds the timestamp, the event name, the event id, the HTTP status, the latency in milliseconds, the attempt number and an error string with IP addresses stripped out. Request and response bodies are not stored. Read the log at Credit System → API & Webhooks or at GET /wp-json/pcs/v1/webhooks/{id}/deliveries.
URL rules
A webhook URL must use the http or https scheme and must pass wp_http_validate_url(), which rejects loopback addresses, RFC 1918 ranges such as 10.0.0.0/8 and 192.168.0.0/16, and link-local addresses such as 169.254.169.254. That check runs at save time and again at delivery time. A duplicate URL on a second active webhook is rejected, and test pings are rate-limited to 10 per 5 minutes per admin user.
What this doesn’t do
- No ordering guarantee across events. Each delivery is an independent background job, and a retried event arrives after events triggered later.
- No server-side replay. The delivery log records outcomes only, so an unaccepted payload cannot be re-sent.
- No delivery to private or loopback addresses, which rules out testing against
localhost. Use a public tunnel endpoint during development. - No
badge.earnedevent unless the Rewards gamification toggle is on. - No filtering beyond the event list. A subscriber to
credit.addedreceives every addition, and thetypefield distinguishes a purchase from a refund.