- an experience your app owns with at least one priced action. If you haven’t built one yet, start with Fan experiences and then Payments inside experiences, which covers declaring, pricing and charging actions
- an access token with
read:experiencefor the purchase reads - a webhook endpoint subscribed to the
app.experience.action.*topics, which needread:self. Subscribe to webhooks covers that
How a charge moves
A purchase goes through five steps whether the fan paid in Fanvue’s dialog or your server charged their wallet. Your app handles the last two, grant and reconcile.
The creator is paid for each action at the standard Fanvue fee and your app receives no share, so a purchase carries no
net or fees. Who gets paid has the price limits.
When to grant and when to revoke
Grant an action once perpurchaseReference, and only when the purchase is succeeded, whether that comes from the app.experience.action.payment.succeeded event or from a read. Revoke the grant when the purchase becomes refunded, disputed or cancelled. The purchase-result message your fan surface receives only tells your UI what to show. Fulfilment rule is the canonical statement for handlers.
status on a purchase says where the money for that attempt is now.
Handle the three reversal values the same way. A purchase stays
succeeded while a dispute is open, and after a refund attempt that did not complete, because in both cases the money stayed with the creator. Only the app.experience.action.dispute.* events tell you a purchase is contested.
Handle events in any order
Fanvue delivers webhooks at least once and in no guaranteed order, as Delivery, retries and idempotency describes. For experience actions that means:- A duplicate delivery carries the same envelope
id. A grant keyed onpurchase_referencemakes it harmless. - A
pendingcan arrive after thesucceededfor the same reference. Don’t let it undo the grant. If events for one reference disagree, read the purchase back and act on itsstatus. app.experience.action.refund.createdcan arrive before you have processed thesucceeded. Record the reversal against thepurchase_referenceeven when there is nothing to revoke yet, and check for it before you grant. A refund and a later chargeback on one purchase are two events with two ids, so make revocation idempotent too.
app.experience.action.payment.succeeded is replayed when it cannot be handed over. The other five events are best effort, so the purchase list is how you find the ones you missed.
What to store
Key your purchases table onpurchaseReference, because every event and every read carries it. Store status next to whether you granted the action, since reconciliation compares the two. Keep invoiceNumber to quote to Fanvue support and for accounting. Keep clientReferenceId to join the purchase to your own record, such as the round the fan was playing. buyerUuid is the same value as fanUuid from the token exchange, so it joins purchases to the sessions your fan surface served. walletBalance on a charge response is a display snapshot and not worth persisting.
Read one purchase
GET /experiences/{experienceUuid}/action-purchases/{purchaseReference} (read:experience) returns one purchase by the reference an event carried. Use it when a handler needs to confirm a purchase before acting.
404 on a reference that arrived in one of your own events means the request is wrong, not that the purchase is gone. Check that the path uses the experience from data.experience.uuid, and that the token was issued to the app that owns that experience.
List purchases
GET /experiences/{experienceUuid}/action-purchases (read:experience) lists every attempt on one of your experiences, newest first, including ones that never settled. limit defaults to 50 with a maximum of 100. Paging is keyset: pass nextCursor from a response back as cursor. A purchase created while you page does not shift a row onto or off a later page.
404 for anything outside your app: an experience owned by another app, or a reference that belongs to another app, another experience or nothing at all.
Reconcile on a schedule
Run this pass for each experience your app owns. It finds thesucceeded purchases whose event you never received and the reversals that arrived as best-effort events.
- Call the list with no
cursor, which starts at the newest purchase, andlimit=100. Pass eachnextCursorback until it isnull. Walk to the end each time. A reversal changesstatuswithout moving the row, so stopping at purchases older than your last pass misses revocations. If a page answers502, retry it with the samecursor. - Grant each
succeededpurchase that has no grant recorded against itspurchaseReference. Its event was missed. - Revoke each grant whose purchase now reads
refunded,disputedorcancelled. - Skip
pendingandfailed. Nothing is owed on either.
Test without a real charge
There is no sandbox, and Test your app explains what that means for a build. Eachapp.experience.action.* topic has a test payload, so you can exercise a handler without a charge. Send one with the Test button in the Events tab or with POST /webhooks/test, as Test a destination describes. The Events tab refuses the test until the app owner has authorised the app with read:self. Test payloads carry TEST- prefixed ids inside data and describe no real purchase, so skip the read-back for a TEST- reference.
Action purchase resource
Both reads return this resource.POST /experiences/{experienceUuid}/action-purchases, the wallet charge, returns it with one extra field, walletBalance.
Webhook field mapping
The events use snake_case and the reads use camelCase.purchase_reference, external_action_id, client_reference_id and paid_at map to the camelCase field of the same name. The fields that differ:
App events: experience actions has every payload.
Refunds and disputes
Refunds have no API on this rail.app.experience.action.refund.created fires when a settled purchase is reversed, with the same purchase_reference and a reason of refund, chargeback or cancel. The read also records reversals of attempts that never settled, so a change of status on the list is the signal to act on whether or not an event arrived. Revoke only what your app granted against that reference. A refunded paid-access unlock loses access on the fan’s next launch without any action from you.
app.experience.action.dispute.flagged and app.experience.action.dispute.created are advisory. The money is still the creator’s while a dispute is open, and the purchase still reads succeeded. Dispute resource has the payloads.