Webhook configuration
Create webhook endpoints, filter deliveries by product or product tag, predict which events are sent or skipped, and troubleshoot missing events.
Webhook configuration
A webhook endpoint receives the event types it subscribes to. You can also give it a product filter, so it only receives subscribed events that involve selected catalog products or product tags. An endpoint without a filter receives every event type it subscribes to, and existing endpoints keep working that way with no migration.
API operations
This guide uses these operations. Each links to its entry in the API reference.
| Operation | Route |
|---|---|
| List Webhooks | GET /v1/webhooks |
| Create Webhook | POST /v1/webhooks |
| Get Webhook | GET /v1/webhooks/{id} |
| Update Webhook | PATCH /v1/webhooks/{id} |
| List Products | GET /v1/products |
| List Webhook Events | GET /v1/webhooks/events |
| Resend Webhook Event | PUT /v1/webhooks/events/{id}/resend |
| Resend Webhook Events | POST /v1/webhooks/events/resend |
Filter fields
filter is an object with two optional sets:
| Field | Type | Rules |
|---|---|---|
productIds | array of strings, optional | Up to 50 unique IDs of active products in your catalog, each a version 4 UUID. |
productTags | array of strings, optional | Up to 50 unique tags, each 1 to 64 characters, found on your active products. |
Tags are exact and case-sensitive: Premium and premium are different tags. Flo does not trim spaces or otherwise normalize a tag, so copy each value exactly as the catalog returns it.
How matching works
A filter narrows which events of its subscribed types an endpoint receives. It never adds an event type: an event the endpoint does not subscribe to is not sent, whatever its products.
When Flo publishes an event, it records which catalog products the event involves, with each product's tags at that moment. For example, a subscription event involves the subscription's product, and a payment event involves the products in the checkout behind the payment. Flo then checks each of those products on its own:
- Within a set, any one value is enough. A product matches
productIdswhen its ID is selected, and matchesproductTagswhen it carries at least one selected tag. - When both sets are populated, the same product must match both: its ID is selected and it carries a selected tag.
- The event matches when at least one of its products matches. A set that is empty or left out does not restrict anything.
| Endpoint filter | Event involves | Outcome |
|---|---|---|
null (unfiltered) | Any products, or none | Sent |
productIds: A or B | Product B | Sent |
productIds: A or B | Product C | Skipped: filter_not_matched |
productTags: Premium or launch-2026 | A product tagged launch-2026 | Sent |
productTags: Premium | A product tagged premium | Skipped: filter_not_matched |
productIds: A, productTags: Premium | Product A tagged Premium | Sent |
productIds: A, productTags: Premium | Product A tagged standard, and product B tagged Premium | Skipped: filter_not_matched |
productIds: A, productTags: Premium | Product C tagged Premium, and product A tagged Premium | Sent |
| Any filter | No catalog product | Skipped: filter_context_missing |
Before the filter, Flo checks the endpoint itself and records the first reason that applies, in this order: endpoint_created_after_event, endpoint_disabled, event_not_subscribed, and only then the filter. An event type the endpoint does not subscribe to is therefore recorded as event_not_subscribed, never as a filter skip.
Events without product context
A filtered endpoint can only match an event that Flo could trace to catalog products. When it cannot, the endpoint skips the event with filter_context_missing. For example, a dispute or Pre-Dispute Alert that is not yet linked to a Flo payment (its paymentIds is empty) has no product context. A later event for the same case is matched normally once the case is linked.
Unfiltered endpoints never need product context, so they receive these events as before. If an integration must see every dispute and Pre-Dispute Alert event, subscribe to those events on an unfiltered endpoint.
Find product IDs and tags
A filter uses the IDs and tags of products in your own catalog. List them with GET /v1/products?status=active; there is no separate endpoint for filter choices. The list is paginated, so request up to 100 products at a time and increase page until you reach the pages value in the response. tags can be null for a product without tags.
curl "https://api.flopay.com/v1/products?status=active&limit=100&page=1" \
-u "$FLOPAY_CLIENT_ID:$FLOPAY_API_TOKEN" \
| jq '[.data[] | {id, name, code, tags}]'[
{
"id": "4f62c6da-4c0e-4c7e-9fd8-8794134d86d5",
"name": "Pro plan",
"code": "pro-monthly",
"tags": ["Premium"]
},
{
"id": "8d1f3b52-6a0e-4d9b-b2c4-17e5a9f0c3d8",
"name": "Team plan",
"code": "team-monthly",
"tags": ["standard", "launch-2026"]
},
{
"id": "e3a9c7f1-0b2d-4e6f-8a1c-9d4b7e2f5a30",
"name": "Starter pack",
"code": "starter-pack",
"tags": null
}
]The create and update examples below use these products.
Create an endpoint
Send POST /v1/webhooks with the endpoint url, the events it subscribes to, and an optional filter. Authenticate as described in Subscribing To Events. Each example below subscribes to the same events, so only the filter changes.
Products only
This endpoint receives subscribed events that involve the Pro plan or the Team plan:
curl -X POST https://api.flopay.com/v1/webhooks \
-u "$FLOPAY_CLIENT_ID:$FLOPAY_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/flopay",
"events": ["subscription.created", "subscription.renewed", "item.purchased"],
"filter": {
"productIds": [
"4f62c6da-4c0e-4c7e-9fd8-8794134d86d5",
"8d1f3b52-6a0e-4d9b-b2c4-17e5a9f0c3d8"
]
}
}'Product tags only
This endpoint receives subscribed events that involve a product tagged Premium or launch-2026, which in the catalog above means the Pro plan or the Team plan:
curl -X POST https://api.flopay.com/v1/webhooks \
-u "$FLOPAY_CLIENT_ID:$FLOPAY_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/flopay",
"events": ["subscription.created", "subscription.renewed", "item.purchased"],
"filter": {
"productTags": ["Premium", "launch-2026"]
}
}'Products and tags together
This endpoint receives subscribed events that involve the Pro plan or the Team plan, and only when that same product is tagged Premium. In the catalog above only the Pro plan carries Premium, so Team plan events are skipped:
curl -X POST https://api.flopay.com/v1/webhooks \
-u "$FLOPAY_CLIENT_ID:$FLOPAY_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/flopay",
"events": ["subscription.created", "subscription.renewed", "item.purchased"],
"filter": {
"productIds": [
"4f62c6da-4c0e-4c7e-9fd8-8794134d86d5",
"8d1f3b52-6a0e-4d9b-b2c4-17e5a9f0c3d8"
],
"productTags": ["Premium"]
}
}'Unfiltered
Leave filter out and the endpoint receives every event it subscribes to:
curl -X POST https://api.flopay.com/v1/webhooks \
-u "$FLOPAY_CLIENT_ID:$FLOPAY_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/flopay",
"events": ["subscription.created", "subscription.renewed", "item.purchased"]
}'On create, "filter": null, "filter": {}, and a filter whose arrays are all empty have the same effect: the endpoint is stored with filter: null.
The response
201 Created returns the stored endpoint. For the products and tags example:
{
"id": "b7e2c1d4-5a3f-4e8b-9c6d-2f1a0e9b8c7d",
"clientId": "0f9c8b7a-6d5e-4c3b-a2f1-e0d9c8b7a6f5",
"url": "https://example.com/webhooks/flopay",
"events": ["subscription.created", "subscription.renewed", "item.purchased"],
"filter": {
"productIds": [
"4f62c6da-4c0e-4c7e-9fd8-8794134d86d5",
"8d1f3b52-6a0e-4d9b-b2c4-17e5a9f0c3d8"
],
"productTags": ["Premium"]
},
"signingSecret": "flo_whsec_...",
"failedSendAt": null,
"disabledAt": null,
"createdAt": "2026-09-11T10:15:00.000Z",
"updatedAt": "2026-09-11T10:15:00.000Z"
}filter is always present in endpoint responses, and null means the endpoint is unfiltered. Keep the signingSecret on your server to verify signatures.
Review active filters
GET /v1/webhooks lists your endpoints, newest first, in the shared paginated envelope. Each endpoint carries its filter, so you can see at a glance which endpoints are filtered:
curl https://api.flopay.com/v1/webhooks \
-u "$FLOPAY_CLIENT_ID:$FLOPAY_API_TOKEN"{
"data": [
{
"id": "b7e2c1d4-5a3f-4e8b-9c6d-2f1a0e9b8c7d",
"clientId": "0f9c8b7a-6d5e-4c3b-a2f1-e0d9c8b7a6f5",
"url": "https://example.com/webhooks/flopay",
"events": ["subscription.created", "subscription.renewed", "item.purchased"],
"filter": {
"productIds": [
"4f62c6da-4c0e-4c7e-9fd8-8794134d86d5",
"8d1f3b52-6a0e-4d9b-b2c4-17e5a9f0c3d8"
],
"productTags": ["Premium"]
},
"signingSecret": "flo_whsec_...",
"failedSendAt": null,
"disabledAt": null,
"createdAt": "2026-09-11T10:15:00.000Z",
"updatedAt": "2026-09-11T10:15:00.000Z"
},
{
"id": "3c9a7e15-2b4d-4f6a-8e1c-5d7b9a3f2e60",
"clientId": "0f9c8b7a-6d5e-4c3b-a2f1-e0d9c8b7a6f5",
"url": "https://example.com/webhooks/flopay-disputes",
"events": ["dispute.created", "dispute.updated", "dispute.won", "dispute.lost"],
"filter": null,
"signingSecret": "flo_whsec_...",
"failedSendAt": null,
"disabledAt": null,
"createdAt": "2026-06-02T08:30:00.000Z",
"updatedAt": "2026-06-02T08:30:00.000Z"
}
],
"page": 1,
"limit": 20,
"pages": 1,
"total": 2
}To read one endpoint, send GET /v1/webhooks/{id}. It returns the same object as the create response, or 404 when the endpoint does not exist or belongs to another account.
curl https://api.flopay.com/v1/webhooks/b7e2c1d4-5a3f-4e8b-9c6d-2f1a0e9b8c7d \
-u "$FLOPAY_CLIENT_ID:$FLOPAY_API_TOKEN"Change or remove a filter
Send PATCH /v1/webhooks/{id}. What happens to the filter depends on what the request contains:
| Request | Result |
|---|---|
No filter property | The saved filter is kept, so an update that only changes url or events never adds or removes one. |
"filter": null, "filter": {}, or a filter with only empty arrays | The filter is removed and the endpoint is unfiltered again. |
A filter with values | The filter is replaced. A set you leave out is removed, so send both sets to keep both. |
This request replaces the products and tags filter from the create example with a tags-only filter. The endpoint now receives subscribed events for any product tagged Premium or launch-2026, and the two product IDs are no longer part of its filter:
curl -X PATCH https://api.flopay.com/v1/webhooks/b7e2c1d4-5a3f-4e8b-9c6d-2f1a0e9b8c7d \
-u "$FLOPAY_CLIENT_ID:$FLOPAY_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"filter": {
"productTags": ["Premium", "launch-2026"]
}
}'This request removes the filter. The 200 response returns the endpoint with filter: null, and the endpoint receives every event it subscribes to again:
curl -X PATCH https://api.flopay.com/v1/webhooks/b7e2c1d4-5a3f-4e8b-9c6d-2f1a0e9b8c7d \
-u "$FLOPAY_CLIENT_ID:$FLOPAY_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"filter": null
}'Like any update, a filter change also clears failedSendAt and disabledAt, which re-enables an endpoint that was disabled, as described under Retries.
Configure filters in the Dashboard
In the FloPay Dashboard, open Settings > Webhooks. Client owners and admins can manage webhook endpoints there. The Dashboard applies the same filter rules as the API.
Create or edit a filtered endpoint
- Select Add Webhook for a new endpoint, or Update on an existing endpoint's row.
- Enter the Endpoint URL and select the Events the endpoint subscribes to.
- Under Product filter, choose Filtered. Choose Unfiltered instead to receive every subscribed event.
- Select values under Products, listed by name and code, and under Product tags. Search narrows either list without hiding the values you have selected, and each list holds up to 50 selections.
- Read the Delivery review. It states what the endpoint will receive, including that events without product context are skipped.
- Select Create for a new endpoint, or Save for an existing one.
If you choose Filtered without selecting anything, the form asks you to select at least one product or tag, or to choose Unfiltered.
Review filters in the endpoint list
The Product filter column shows the state of every endpoint:
- Unfiltered, with
Every subscribed event. - Filtered, with a summary such as
Any of 2 products,1 tag, or2 products and 1 tag on the same product.
Return an endpoint to unfiltered
- Select Update on the endpoint's row.
- Under Product filter, choose Unfiltered. The Delivery review warns that saving removes the product filter and that the endpoint will receive every subscribed event again.
- Select Save. The Dashboard sends
filter: null, and the Product filter column shows Unfiltered.
Saving changes to only the URL or events never changes a saved filter.
Unavailable values and large catalogs
- A saved product or tag that is no longer in your active catalog stays selected and is marked Unavailable. It is kept while you only change the URL or events. To change the filter, clear the unavailable values first or choose Unfiltered; the Dashboard asks you to do this instead of sending a request that would fail.
- The Dashboard lists choices for catalogs of up to 2,000 active products. For a larger catalog it keeps the saved filter as it is: choose Unfiltered to remove it, or change it through the API.
- If the product and tag choices cannot load, select Try again. Until they load, you can keep a saved filter as it is or save a new endpoint unfiltered.
Invalid and unavailable values
Flo validates a filter whenever a create or update request includes one.
A malformed filter returns the standard validation 400 Bad Request, whose message array describes each problem. That covers a filter that is not an object, a set that is not an array, a product ID that is not a version 4 UUID, an empty tag or one longer than 64 characters, a value repeated within a set, and a set with more than 50 values.
A well-formed value that is not available in your active catalog returns one generic response instead. That covers a product ID that does not exist, belongs to an inactive or deleted product, or belongs to another account, and a tag that no active product in your catalog carries:
{
"message": "Webhook filter contains unavailable product selectors",
"error": "Bad Request",
"statusCode": 400
}The response is identical in every case and does not say which value was unavailable, so it never reveals anything about another account's catalog. To fix the request, list your active products again with GET /v1/products?status=active and remove any value that is no longer listed.
Once saved, filter values are durable:
- An update that omits
filterkeeps the saved filter as it is, even when one of its values has since become unavailable. You only need to remove that value when you next change the filter. - Flo does not recheck saved values during delivery. If a selected product is later deactivated or deleted, events that still involve it, such as a renewal of an existing subscription to it, continue to match.
- Tags are compared as they were when each event was published. Removing a tag from a product stops later events for that product from matching the tag, and adding a tag affects later events only.
Skipped versus failed deliveries
Flo records a delivery for every event and endpoint pair, including events a filter skips, so you can always see what happened. Read them with GET /v1/webhooks/events and GET /v1/webhooks/events/{id}, described under Delivery Records, Replay, And Resend. A filter skip is intended behavior, not an error, and it looks different from a delivery that failed:
| What to check | Skipped by a filter | Failed delivery |
|---|---|---|
status | skipped | retrying while attempts continue, then failed |
skipReason | filter_not_matched or filter_context_missing | null |
| Request to your endpoint | None | One per attempt, with the last responseCode recorded |
retryCount | 0 | The number of attempts made |
| Automatic retries | None | With backoff, as described under Retries |
Endpoint failedSendAt and disabledAt | Unchanged | failedSendAt records the first failure, and sustained failures set disabledAt |
| Resend | Never: 409 Conflict for one delivery, or listed in skippedIds in bulk | Allowed |
Retries and resends
Flo makes each filter decision once, when it first routes an event to your endpoints, and never recomputes it. A filter change therefore applies only to events routed after you save it.
- A delivery that matched keeps its
eventIdand payload, is signed the same way, retries automatically, and can be resent, even if you later change the filter or the product's tags. - A delivery skipped by a filter is never sent.
PUT /v1/webhooks/events/{id}/resendreturns409 Conflictfor it, andPOST /v1/webhooks/events/resendlists it inskippedIdswithout queuing it. Changing or removing the filter later does not make it sendable. - Deliveries skipped for
endpoint_created_after_event,endpoint_disabled, orevent_not_subscribedresend as before. Flo recorded them before it reached the filter, and a resend never applies the filter, so they are sent whether or not they match the endpoint's current filter. Narrow a bulk resend withskipReasonandeventTypewhen you only want some of them.
Existing endpoints
Product filters are opt-in, and existing endpoints need no migration:
- Every endpoint created before filters were available returns
filter: nulland keeps event-type-only delivery, receiving every event type it subscribes to, until you set a filter. - Endpoints you create without a filter, or return to unfiltered, behave the same way.
- An update that omits
filternever adds one, so integrations that only updateurloreventsare unaffected. - Payloads,
eventIdvalues, andFlo-Signaturesigning are unchanged. The product context Flo uses for routing is never added to the payload. - Delivery records gain the nullable
skipReason, and its two new values appear only for endpoints that use a filter.
Troubleshoot a missing event
Flo records one delivery for each event and endpoint pair, so start from the delivery record. List the deliveries for the event type, narrowed by the related Flo record when you know it, and add relation=webhook to see which endpoint each delivery belongs to:
curl "https://api.flopay.com/v1/webhooks/events?eventType=item.purchased&transactionUuid=6a1d4e2b-9c3f-4b7a-8e5d-0f2c1b3a4d5e&relation=webhook" \
-u "$FLOPAY_CLIENT_ID:$FLOPAY_API_TOKEN"Then read the status and skipReason of the delivery for your endpoint:
| What you find | What it means | What to do |
|---|---|---|
| No delivery for the endpoint | Flo has not recorded the event for your account yet. Delivery is asynchronous, so the record can lag behind the activity. | Check again shortly, and confirm in the event catalog that the activity produces this event type. |
skipped, filter_not_matched | Filter exclusion: none of the event's products matched the filter when Flo routed the event. | Compare the endpoint's filter with the event's products and their tags at that time, remembering that tags are case-sensitive and that an ID and a tag must match on the same product. Change the filter for future events; this delivery cannot be resent. |
skipped, filter_context_missing | Missing product context: Flo could not trace the event to catalog products, as with a dispute that is not yet linked to a payment. | Subscribe to these event types on an unfiltered endpoint if you need every one of them; this delivery cannot be resent. |
skipped, event_not_subscribed | Event-type exclusion: the endpoint does not subscribe to this event type, so the filter was never checked. | Send PATCH /v1/webhooks/{id} with the complete events list, including this type, then resend the delivery if you need it. |
skipped, endpoint_disabled | Endpoint disabled: sustained delivery failures disabled the endpoint before this event. | Fix the endpoint, send any PATCH /v1/webhooks/{id} to re-enable it, then resend the delivery. |
skipped, endpoint_created_after_event | The endpoint was created after Flo published the event, so it was never eligible to receive it. | No action is needed; the endpoint receives events published after it was created. |
pending or retrying | Delivery in progress: Flo is still attempting delivery, and responseCode shows your endpoint's last response. | Wait for the next attempt, and fix the endpoint if it is returning errors. |
failed | Delivery failure: Flo sent the event, but your endpoint returned a permanent 4xx or kept failing until retries were exhausted. | Check responseCode and responseBody, fix the endpoint, then resend the delivery. |
success | Flo delivered the event and your endpoint returned a 2xx. | Look for the delivery's eventId in your own logs; a handler that deduplicates by eventId ignores a repeat. |
To list every delivery your filters skipped, across all of your endpoints:
curl --globoff "https://api.flopay.com/v1/webhooks/events?skipReason[in]=filter_not_matched,filter_context_missing" \
-u "$FLOPAY_CLIENT_ID:$FLOPAY_API_TOKEN"Limitations
- Filters match catalog product IDs and exact product tags only. They cannot match
checkoutMetadata, amounts, currencies, customers, or any other payload field, and they support no expressions, wildcards, prefixes, negation, or case-insensitive matching. - Filtering adds no event types. A filter only narrows which events of its subscribed types an endpoint receives.
- Filtering does not change how Flo signs deliveries, the retry policy, ordering, or at-least-once delivery, all described under Delivery.
- A delivery that a filter skipped cannot be sent later, even after you change or remove the filter.
- Each set holds at most 50 values, and the Dashboard lists choices for catalogs of up to 2,000 active products.