Docs / Webhooks
For Data Feed and FFL Checkout stores
Reference · Store API v1
Webhooks
Have your store's changes sent to your own URL as they happen, instead of asking GET /changes for them, and check that each one came from FFL Monkey.
- Per store
- One webhook URL
- Permission
- Webhook, for Data Feed and FFL Checkout stores
- Signed
- HMAC-SHA256 in
X-FFL-Signature - Answer
- Any 2xx within 10 seconds
Set it
A store has one webhook URL. Set it with PUT /webhook, using a key with Webhook: Read and write, which a key of a Data Feed or an FFL Checkout store can hold.
- The secret is shown once, in the answer to the PUT that created the webhook. It signs every delivery. Changing the URL keeps it; for a new one, DELETE /webhook and set the URL again.
- Test it before you rely on it with POST /webhook/test, which sends one signed
webhook.tested event and tells you what your URL answered.
- A store whose Data Feed plugin is connected already has its webhook. The plugin set it up and receives every change, so
PUT /webhook answers 409 webhook_managed_by_plugin. GET /webhook shows it, with managed_by plugin.
- A webhook is optional. Reading GET /changes every few minutes works as well.
What is sent
Your store's own changes, never another store's, and the same for every key: a key that sets the URL decides where all of them go. For a Data Feed store:
| About | event_type |
| Products and prices | product.*, merchant_price.* |
| Pricing rules | pricing_rule.*, pricing_settings.* |
| Restrictions | restriction.* |
| Distributors | distributor_account.*, license.distributor_selection.* |
| In-house inventory | merchant_inventory.* |
| Your licence and subscription | license.activated, license.suspended, license.canceled, subscription.* |
For an FFL Checkout store: its FFL orders (ffl_selection.*, ffl_order.*), and its licence and subscription, as above.
Each change is the object GET /changes returns, and its payload is the state after the change. New kinds of change may appear: ignore a type you do not know. A kind added later reaches a webhook already set, without setting it again.
Each delivery
A POST to your URL with a JSON body {"events": [change]}, one change per request. It carries these headers:
| Header | What it is |
| X-FFL-Event | The change's event_type. |
| X-FFL-Event-Id | Its event_id. |
| X-FFL-Delivery-Id | This delivery's id, the same on every retry. Quote it to support. |
| X-FFL-Cursor | Its cursor. |
| X-FFL-Timestamp | When this attempt was sent, in Unix seconds. |
| X-FFL-Signature | sha256= and the hex HMAC-SHA256 of {X-FFL-Timestamp}.{the raw body}, keyed with your secret. |
Check the signature
Before you trust a delivery, prove it came from FFL Monkey:
- Read the body raw, exactly as it arrived, before any JSON parsing.
- Join
X-FFL-Timestamp, a dot and the raw body, and compute its HMAC-SHA256 with your secret, in hex. Put sha256= in front.
- Compare that with
X-FFL-Signature using a constant-time comparison, never ==.
- Refuse a timestamp more than 5 minutes from your clock, so an old delivery cannot be sent to you again.
Answer a delivery that fails the check with 401, and never apply it. The code column has the check in each language.
Apply each change once
- By
event_id. The same change can arrive twice, after a retry, and a change you already read with GET /changes can arrive later by webhook. Keep the event_ids you applied, and answer 200 to one you have seen.
- Never go back. Changes can arrive out of order. Skip one whose
cursor is not above the last one you applied for the same aggregate_type and aggregate_id. A merchant_price.recomputed change is a batch: its aggregate_id names the batch, and payload.products holds many products. Keep the last cursor you applied per product_id, and skip a product in it whose last applied cursor is above the change's.
Retries and the pause
Answer any 2xx within 10 seconds, once the change is stored. Anything else is a failure: another status (4xx counts the same as 5xx), a redirect, or no answer in time. A failed delivery is tried again after 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours and 24 hours, seven attempts in all.
- Degraded: after the first failure, GET /webhook says
degraded. New changes are still sent, and the first 2xx makes it active again.
- Paused: after 50 failures in a row, it pauses, with
paused_reason delivery_health. Nothing is sent, and changes made while it is paused are not kept for it.
- To resume it, fix the endpoint, then send PUT /webhook again, while your subscription is active. Then read GET /changes from your last cursor to catch up on what was not sent.
- A webhook paused for any other reason, such as
billing, is not the API's to lift.
When your subscription is ending
Seven days or fewer before a subscription that is set to end, your webhook gets a subscription.ending_soon change, once for each end date. Its payload holds license_id, billing_status, ends_at and cancel_at_period_end.
It carries no link. Call GET /subscription when a person is about to renew: its renewal_url works for an hour and opens your store's billing, so treat it as a secret and never store, log or email it.