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

  1. Extract the timestamp (t) and signature (v1) from the Stripe-Signature header.
  2. Reject the request if the timestamp is older than 5 minutes (300 seconds) to prevent replay attacks.
  3. Concatenate the timestamp and raw request body with a period separator: {timestamp}.{payload}.
  4. Compute an HMAC-SHA256 hash of the concatenated string using stripe_webhook_secret as the key.
  5. 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:

  1. Queries for events where next_retry_at is in the past and retry_count < MAX_RETRIES.
  2. Processes up to 10 events per run to avoid long-running cron tasks.
  3. On success, sets processed_at to the current time and clears processing_error and next_retry_at.
  4. On failure, increments retry_count, records the error in processing_error, and calculates the next next_retry_at based 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