Webhooks
LicenceForge exposes a REST endpoint for receiving incoming Stripe webhooks. Events are verified, deduplicated, and processed idempotently with automatic retry on failure.
Stripe endpoint
POST /wplf/v1/webhooks/stripe
Configure the following URL in your Stripe Dashboard under Developers > Webhooks:
https://your-site.com/wp-json/wplf/v1/webhooks/stripe
Replace your-site.com with the domain where LicenceForge is installed. Stripe will send event payloads to this URL whenever a subscribed event occurs.
Note
For details on which Stripe event types LicenceForge handles and how each maps to licence actions, see Stripe Webhooks (Payments).
Signature verification
Every incoming request is verified using HMAC-SHA256 signature validation. LicenceForge reads the Stripe-Signature header and computes the expected signature using the stripe_webhook_secret stored in your LicenceForge settings.
Verification process
- Extract the timestamp (
t) and signature (v1) from theStripe-Signatureheader. - Reject the request if the timestamp is older than 5 minutes (300 seconds) to prevent replay attacks.
- Concatenate the timestamp and raw request body with a period separator:
{timestamp}.{payload}. - Compute an HMAC-SHA256 hash of the concatenated string using
stripe_webhook_secretas the key. - Compare the computed hash against the signature from the header using a timing-safe comparison.
If verification fails at any step, the endpoint returns a 403 Forbidden response and the event is not processed.
Warning
Never expose your stripe_webhook_secret in client-side code or version control. Rotate it immediately if compromised, then update the value in LicenceForge > Settings > Payments.
Idempotent processing
Stripe may deliver the same event more than once. LicenceForge guarantees idempotent processing by storing every received event's event_id in the wplf_webhook_events table with a UNIQUE constraint on the event_id column. The insert uses INSERT IGNORE, so duplicate deliveries are silently skipped.
wplf_webhook_events table
| Column | Type | Description |
|---|---|---|
id |
BIGINT (PK) | Auto-incrementing primary key. |
event_id |
VARCHAR (UNIQUE) | Stripe event ID (e.g. evt_1abc2def3ghi). Ensures idempotency. |
event_type |
VARCHAR | Stripe event type (e.g. invoice.payment_succeeded). |
source |
VARCHAR | Webhook source identifier (e.g. stripe). |
processed_at |
DATETIME | Timestamp when processing completed successfully. NULL if pending or failed. |
processing_error |
TEXT | Error message from the most recent failed processing attempt. NULL on success. |
retry_count |
INT | Number of processing attempts so far. Starts at 0. |
next_retry_at |
DATETIME | Scheduled time for the next retry attempt. NULL if processing succeeded or retries exhausted. |
payload |
LONGTEXT | Full JSON payload from Stripe, stored for audit and retry purposes. |
created_at |
DATETIME | Timestamp when the event was first received. |
Payload structure
Below is an example of the JSON payload that Stripe sends and LicenceForge stores in the payload column:
{
"id": "evt_1abc2def3ghi",
"object": "event",
"type": "invoice.payment_succeeded",
"created": 1708531200,
"data": {
"object": {
"id": "in_1xyz2abc3def",
"customer": "cus_A1B2C3D4E5",
"subscription": "sub_F6G7H8I9J0",
"amount_paid": 9900,
"currency": "usd",
"status": "paid",
"lines": {
"data": [
{
"price": {
"id": "price_K1L2M3N4O5",
"product": "prod_P6Q7R8S9T0",
"recurring": {
"interval": "year"
}
},
"quantity": 1
}
]
}
}
}
}
Retry mechanism
When event processing fails (e.g. due to a temporary database error or external service timeout), LicenceForge schedules the event for automatic retry using exponential backoff.
Retry schedule
| Attempt | Delay | Cumulative wait |
|---|---|---|
| 1st retry | 1 minute | 1 minute |
| 2nd retry | 5 minutes | 6 minutes |
| 3rd retry | 15 minutes | 21 minutes |
| 4th retry | 1 hour | 1 hour 21 minutes |
| 5th retry | 4 hours | 5 hours 21 minutes |
The maximum number of retries is 5 (MAX_RETRIES = 5). After all retries are exhausted, the event remains in the table with processing_error populated and next_retry_at set to NULL. Failed events can be inspected and manually reprocessed from the admin panel.
Retry cron job
The wplf_webhook_retry cron event runs every 5 minutes. On each run it:
- Queries for events where
next_retry_atis in the past andretry_count < MAX_RETRIES. - Processes up to 10 events per run to avoid long-running cron tasks.
- On success, sets
processed_atto the current time and clearsprocessing_errorandnext_retry_at. - On failure, increments
retry_count, records the error inprocessing_error, and calculates the nextnext_retry_atbased on the backoff schedule.
Note
The retry cron relies on WordPress cron (wp_cron). If your site uses a system-level cron to trigger wp-cron.php, ensure it runs at least every 5 minutes for timely retries.
Response codes
| Code | Meaning | Description |
|---|---|---|
200 |
OK | Event received and processed (or already processed previously). |
202 |
Accepted | Event received but processing failed. Scheduled for retry. |
400 |
Bad Request | Malformed payload or missing required fields. |
403 |
Forbidden | Signature verification failed or timestamp outside tolerance. |
Next steps
- Stripe Webhooks (Payments) -- Event type details and licence action mappings
- Stripe Integration -- Setting up Stripe as a payment gateway
- API Key Management -- Securing your API endpoints
- Cron Jobs -- All scheduled tasks including webhook retry