Charge one action from the fan's wallet balance

Charges one of your experience's priced actions straight to the fan's Fanvue wallet balance. No payment dialog is shown — the fan sees nothing, your surface is not interrupted, and the call returns once the money has moved.

That is only possible because the fan agreed to it beforehand. A charge requires a live **spend consent** for this experience, which the fan gives when they top up inside the experience. Without one you get 403 `consent_required`; send the fan back through the in-experience top-up to obtain it. Consent is per app and per experience, and the fan can revoke it.

You name the action, never the amount: `externalActionId` selects an action from this experience's catalogue and Fanvue resolves the price the creator set. Owner app only, and the fan must be one of yours.

**Fulfil on the webhook, never on the bridge result.** Grant the action when you receive `app.experience.action.payment.succeeded`, keyed on `purchase_reference` so a redelivery grants nothing twice. The `purchaseResult` message your iframe receives from Fanvue is UX only — it tells your surface to stop showing a spinner, and it is never proof that money moved. This read is the reconciliation path: poll it when you think you missed a webhook, or to confirm one you are unsure about.

**`Idempotency-Key` is required** (400 without it). Reuse the same key on a retry: once the first charge settled, the same purchase is replayed; while it is still running you get 409. A key reused for a *different* charge is 409 as well, so pick a fresh one per action. Keys are scoped to your app, the fan and the experience, and remembered for 24 hours.

A fan who cannot afford the action is 402 with their `balance` and the `amount` needed — nothing is charged and no purchase attempt is recorded.
post/experiences/{experienceUuid}/action-purchases

Path parameters

experienceUuidstring uuid required
Example:00000000-0000-4000-8000-000000000010

Headers

X-Fanvue-API-Versionstring required
Example:2025-06-26

API version to use for the request

Idempotency-Keystring required

Opaque key that makes this charge safe to retry. Required: the charge moves money, so a retry without one could debit the fan twice.

Request body

externalActionIdstring required

Your own stable key for the action to charge, as you declared it. A selector inside this experience's catalogue — never a price, and never an authorisation. The amount charged is the one the creator set.

fanUuidstring uuid required

The fan to charge, as returned by the token exchange. They must have given spend consent for this experience.

clientReferenceIdstring

Your own reference for this charge, carried through the purchase and echoed on the settled-purchase webhook.

Example request

{
  "externalActionId": "spin",
  "fanUuid": "00000000-0000-4000-8000-000000000003",
  "clientReferenceId": "round-42"
}

Response

The settled purchase, and the fan's remaining wallet balance

purchaseReferencestring required

The expact_… reference minted with the purchase attempt. THE fulfilment idempotency key: grant the action at most once per reference, and only for a purchase whose status is succeeded.

experienceUuidstring uuid required

The experience the action belongs to.

externalActionIdstring required

The app's own stable key for the action that was bought.

clientReferenceIdstring nullable required

The app-supplied reference carried through the purchase, or null when the app supplied none.

buyerUuidstring uuid required

UUID of the fan who bought the action. The only identifier of the buyer this surface carries — no handle, no display name, no email.

amountinteger required

Price in minor currency units (cents), frozen at the purchase attempt. A later re-pricing of the action does not move it.

currencystring required

ISO 4217 code of the amount. USD only today.

status'pending' | 'succeeded' | 'failed' | 'refunded' | 'disputed' | 'cancelled' required

Where the money for this purchase attempt ended up. Fulfil only on succeeded. refunded, disputed and cancelled all mean the money is not the creator's, so revoke on any of them — the difference between them only records how the reversal was written, not whether to act. refunded is a reversal carrying a refund reference (a settled payment paid back, or an authorisation voided before capture); disputed is a chargeback that was lost and reversed; cancelled is a reversal carrying neither reference (a delayed capture that failed, or the uncaptured-invoice sweeper). An open dispute that has not been lost is still succeeded here — the money has not moved — so watch app.experience.action.dispute.* for that, not this field. A refund the processor accepted and then rejected also reports succeeded again: this field is where the money ended up, not what was attempted.

invoiceNumberstring required

Fanvue invoice number for the charge. For support and accounting, not fulfilment.

createdAtstring date-time required

When the purchase attempt was opened.

paidAtstring date-time nullable required

When the charge settled, or null while it has not.

walletBalanceinteger required

The fan's remaining wallet balance in minor units (cents, USD) after this charge.

Example response

{
  "purchaseReference": "expact_5c9a1f0e-9f3f-4c2a-8f31-1d0f6a2b7c44",
  "experienceUuid": "00000000-0000-4000-8000-000000000010",
  "externalActionId": "spin",
  "clientReferenceId": "round-42",
  "buyerUuid": "00000000-0000-4000-8000-000000000003",
  "amount": 700,
  "currency": "USD",
  "invoiceNumber": "FVE-20260904-1234",
  "createdAt": "2026-09-04T10:00:00.000Z",
  "paidAt": "2026-09-04T10:00:02.000Z",
  "walletBalance": 1800
}

Changes