Skip to content
Gaurav TiwariDocs

Use Webhooks and the REST API

Version 0.1.0 Updated October 28, 2026

Easy Digital Downloads has no webhook system of its own. So when a sale, a renewal or a license activation should reach Zapier, Make, n8n or your own app, you end up polling, or writing glue code that breaks on the next update.

The Webhooks and REST API module fixes both directions. It sends signed webhooks for order, subscription and license events, and it adds a subscriptions REST API that accepts EDD’s own API keys. Everything on this page comes from the plugin’s code, so the routes, topics and headers are exact.

Turn On the Module

  1. Go to Stackless → Dashboard and switch on Webhooks and REST API. It’s off by default.
  2. Open Stackless → Settings → Webhooks and REST API. The screen has no settings to fill in. It shows your REST base address and links to the Webhooks screen and to Downloads → Tools → API Keys.
  3. Switch on Subscriptions if you want subscription topics and the REST API, and Software Licensing if you want license topics. Order topics only need EDD.
Stackless → Settings → Webhooks and REST API in Stackless for EDD, showing the signature header, the REST API base address and a link to Downloads → Tools → API Keys

Add a Webhook

Webhooks live on their own screen at Stackless → Webhooks. You need the manage_shop_settings capability to open it.

The empty Stackless → Webhooks screen in Stackless for EDD with an Add webhook button and links to Settings and Documentation
  1. Click Add webhook.
  2. Fill in the form (fields below).
  3. Click Save webhook.
  4. Back on the list, click Send test to send a signed ping right away. The notice says “Test delivered” with the HTTP code, or why it failed.

The form fields:

  • Name: a label for the list. Left empty, it uses the host of the delivery URL.
  • Status: Active, Paused or Disabled. Paused webhooks queue nothing. Defaults to Active.
  • Topics: tick All topics or pick single events. At least one is required. Subscription and license topics only appear while those modules are on.
  • Delivery URL: must start with https://, or http:// for a test endpoint.
  • Secret: signs every delivery. Leave it blank and Stackless generates a 40-character secret when you save. On an existing webhook, blank keeps the current one.

The list shows each webhook’s topics, status, a count of failed deliveries in a row and the last delivery. Deliveries opens the 20 most recent attempts with their date, topic, resource ID, attempt number, response code and time taken. Delete removes the webhook and drops its pending deliveries.

Webhook Topics

Order topics come from EDD itself. They fire only for sale orders, never for EDD’s separate refund records.

TopicLabel on the screenFires whenNeeds
order.createdOrder createdEDD adds a sale orderEDD
order.paidOrder paidEDD completes the purchaseEDD
order.updatedOrder status changedan order moves from one status to anotherEDD
order.refundedOrder refundedEDD records a refund on the orderEDD
order.deletedOrder deletedan order is about to be deletedEDD
subscription.createdSubscription createdcheckout or a plan switch creates a subscriptionSubscriptions
subscription.updatedSubscription updatedthe status changes, or an admin, customer or gateway edits itSubscriptions
subscription.renewedSubscription renewal paida renewal order is paidSubscriptions
subscription.payment_failedSubscription renewal payment faileda renewal payment failsSubscriptions
subscription.switchedSubscription switchedthe customer switches plansSubscriptions
subscription.deletedSubscription deleteda subscription is deletedSubscriptions
license.createdLicense key createdan order creates license keys (one delivery per key)Software Licensing
license.updatedLicense key updateda license record changesSoftware Licensing
license.activatedLicense activated on a sitea site is activated on a keySoftware Licensing
license.deactivatedLicense deactivated on a sitea site is removed from a keySoftware Licensing

What a Delivery Looks Like

Each event queues one delivery per active webhook that listens to it. Deliveries run in the background on Action Scheduler, in the group stackless-edd-developer_api, so checkout never waits on your endpoint. The body is built when the delivery runs, so it shows the resource as it is at that moment.

Every delivery is a POST with a JSON body and these headers:

HeaderValue
Content-Typeapplication/json
User-AgentStackless for EDD/{version} Hookshot (WordPress/{version})
X-Stackless-Webhook-Sourceyour site’s home URL
X-Stackless-Webhook-Topicthe topic, such as order.paid
X-Stackless-Webhook-Resourceorder, subscription or license
X-Stackless-Webhook-Eventthe part after the dot, such as paid
X-Stackless-Webhook-IDthe webhook’s ID
X-Stackless-Webhook-Delivery-IDa UUID, new for every attempt
X-Stackless-Webhook-TimestampUnix time of the attempt
X-Stackless-Webhook-Signaturebase64 HMAC-SHA256 of the raw body, keyed with the secret

The body is the resource itself, with no wrapper. Some events add a field:

  • order.updated adds previous_status.
  • order.refunded adds refund_id.
  • subscription.updated adds previous_status when the status changed. Other edits send the subscription without it.
  • subscription.renewed, subscription.payment_failed and subscription.switched add order, a short summary of the renewal or switch order.
  • license.activated and license.deactivated add site_url.
  • Every .deleted topic sends only {"id": 123}, because the record is gone.

The test from Send test uses the topic webhook.ping and sends webhook_id, ping: true, the webhook’s topics and site_url.

The Order Object

  • id, number, status, type, mode, currency
  • subtotal, discount, tax, total, total_refunded as decimal strings
  • gateway, transaction_id
  • customer: id (EDD customer), user_id, email, name
  • billing_address: name, address, address2, city, region, postal_code, country
  • line_items: id, download_id, price_id (null without variable pricing), name, quantity, status, subtotal, discount, tax, total
  • discounts: id, code, amount
  • refunds: id, number, total, date_created
  • subscription_ids: subscriptions this order created or renewed
  • date_created, date_modified, date_completed in ISO 8601 UTC, or null

The Subscription Object

The REST API returns the same shape, so a subscription looks identical whichever way it arrives.

{
  "id": 42,
  "parent_order_id": 1187,
  "status": "active",
  "customer_id": 12,
  "user_id": 7,
  "email": "jordan@example.com",
  "currency": "USD",
  "recurring_total": "29.00",
  "billing_period": "month",
  "billing_interval": 1,
  "renewal_mode": "auto",
  "gateway": "stackless_edd_stripe",
  "gateway_subscription_id": "",
  "gateway_customer_id": "cus_...",
  "trial_end_date": null,
  "next_payment_date": "2026-11-28T09:14:02Z",
  "end_date": null,
  "line_items": [
    {"download_id": 34, "price_id": 2, "name": "Pro plan", "quantity": 1, "amount": "29.00"}
  ]
}
  • status: pending, trial, active, on-hold, pending-cancel, cancelled or expired.
  • billing_period: day, week, month or year.
  • renewal_mode: auto or manual.
  • end_date: null when the subscription runs until cancelled.

The order summary used in subscription events and in the orders route has id, number, relation (parent, renewal or switch), status, currency, total, tax, gateway, transaction_id, date_created and date_completed.

The License Object

  • id, license_key
  • status (with expiry applied) and stored_status (as saved)
  • download_id, price_id, order_id, subscription_id, customer_id, user_id, customer_email
  • activation_limit (0 means unlimited) and activations
  • expires_at, date_created, date_modified in ISO 8601 UTC, or null

License payloads include the full key. To keep keys out of webhooks, unset license_key with the stackless_edd_developer_api_license filter (example below).

Verify the Signature

Compute a base64 HMAC-SHA256 of the raw request body with the webhook secret, then compare it to X-Stackless-Webhook-Signature. Use the raw body exactly as received. Parsing and re-encoding the JSON changes the bytes and breaks the match.

$body     = file_get_contents('php://input');
$secret   = 'your-webhook-secret';
$expected = base64_encode(hash_hmac('sha256', $body, $secret, true));
$received = $_SERVER['HTTP_X_STACKLESS_WEBHOOK_SIGNATURE'] ?? '';

if (!hash_equals($expected, $received)) {
    http_response_code(401);
    exit;
}
$event   = $_SERVER['HTTP_X_STACKLESS_WEBHOOK_TOPIC'] ?? '';
$payload = json_decode($body, true);

Reply with any 2xx status to mark the delivery done. Anything else counts as a failure.

Retries and Auto-Disable

A delivery that doesn’t get a 2xx reply is retried three times. Stackless waits 15 seconds for a reply and doesn’t follow redirects, so a 301 or 302 also counts as a failure.

AttemptWhen
1right after the event
21 minute after attempt 1 fails
310 minutes after attempt 2 fails
41 hour after attempt 3 fails

When all four attempts fail, the webhook’s failure count goes up by one. After five events in a row fail for good, Stackless sets the webhook to Disabled and writes a warning to the debug log. A successful delivery or test resets the count, and so does saving the webhook as Active.

Each retry gets a new Delivery ID. If your endpoint timed out but still did the work, the retry brings the same event again, so make your handler safe to run twice.

Call the Subscriptions REST API

The REST API lives in the stackless-edd/v1 namespace. Its routes exist only while the Subscriptions module is on.

https://yourstore.com/wp-json/stackless-edd/v1/
Method and routeWhat it does
GET /subscriptionsList subscriptions, newest first
GET /subscriptions/{id}One subscription
GET /subscriptions/{id}/ordersIts parent, renewal and switch orders, newest first
POST /subscriptions/{id}/cancelCancel now, or at the end of the paid period
POST /subscriptions/{id}/reactivateWithdraw a pending cancellation, or resume an on-hold subscription

List Parameters

  • status: comma-separated public statuses, such as active,trial. Unknown names are dropped. If none are valid, you get stackless_edd_rest_invalid_status (400).
  • customer: an EDD customer ID.
  • product: a download ID.
  • page: defaults to 1.
  • per_page: defaults to 20, up to 100.

The list response carries X-WP-Total and X-WP-TotalPages headers, like WordPress’s own routes.

Cancel and Reactivate

cancel takes at_period_end, which defaults to true, so the customer keeps access until the paid period ends. Send false to cancel right away. Both actions are recorded as made by an admin in the subscription notes. They return the updated subscription, or a 409 error when the change isn’t allowed from its current status.

curl -u PUBLIC_KEY:TOKEN \
  "https://yourstore.com/wp-json/stackless-edd/v1/subscriptions?status=active,trial&per_page=50"

curl -u PUBLIC_KEY:TOKEN -X POST \
  -H "Content-Type: application/json" \
  -d '{"at_period_end": false}' \
  "https://yourstore.com/wp-json/stackless-edd/v1/subscriptions/42/cancel"

Authenticate With EDD API Keys

The routes accept the API keys EDD already has. Create a key under Downloads → Tools → API Keys for a user who can manage payments, then send it one of two ways:

  • HTTP Basic auth: the public key as the username and the token as the password.
  • Query string: ?key=PUBLIC_KEY&token=TOKEN, the same way EDD’s own API takes them.

Both values are 32 hexadecimal characters. The token is the one EDD shows next to the key, not the secret key. The request then runs as the key’s owner, so every capability check still applies. Signed-in admin requests with WordPress’s REST nonce work too.

EDD keys only authenticate these subscription routes. They don’t open any other WordPress REST route.

Permissions

Reading and writing both need the edit_shop_payments capability by default. Without it, a request gets stackless_edd_rest_forbidden. You can raise either check separately:

add_filter('stackless_edd_developer_api_write_capability', function () {
    return 'manage_shop_settings';
});
Error codeStatusWhen
stackless_edd_rest_forbidden401 or 403the user lacks the capability
stackless_edd_rest_invalid_status400no valid status in status
stackless_edd_rest_subscription_invalid_id404no subscription with that ID
the Subscriptions error code409cancel or reactivate isn’t possible right now

Filters for Developers

FilterArgumentsUse it to
stackless_edd_developer_api_subscriptionarray $data, SubscriptionData $subscriptionchange the subscription JSON (REST and webhooks)
stackless_edd_developer_api_orderarray $data, \EDD\Orders\Order $orderchange the order JSON in webhooks
stackless_edd_developer_api_licensearray $data, array $licensechange the license JSON in webhooks
stackless_edd_developer_api_read_capabilitystring $capabilitychange who can read
stackless_edd_developer_api_write_capabilitystring $capabilitychange who can cancel or reactivate
stackless_edd_developer_api_max_failuresint $max, array $webhookchange the five-failure limit (0 never disables)
stackless_edd_developer_api_deliver_on_copybool $deliver, array $webhook, string $topicallow deliveries from a staging copy
// Keep license keys out of webhook payloads.
add_filter('stackless_edd_developer_api_license', function (array $data) {
    unset($data['license_key']);
    return $data;
});

Good to Know

  • On a copy of your live store, such as a staging site, webhooks aren’t sent. Each one is logged as “Skipped: this site is not the live store.” and the Webhooks screen shows a warning. Troubleshooting explains how a site is confirmed as live.
  • Paused and Disabled webhooks queue nothing. Deliveries already queued for them are dropped when they come due.
  • The same event goes to the same webhook once per request, even if EDD fires its hook twice.
  • If the resource is gone when a delivery runs (other than a delete topic), the attempt is logged as skipped and not retried.
  • Deliveries use WordPress’s safe HTTP request, which refuses local and private network addresses. Use a public URL or a tunnel for local testing.
  • Failed attempts are written to Downloads → Tools → Debug Log under [stackless-edd-developer_api]. Secrets never are.
  • The REST API covers subscriptions only. For orders, customers and downloads, use EDD’s own API. The license server has its own routes, covered in License API.
Share

Was this helpful?

Still stuck? Open a support ticket