---
title: "Charge one action from the fan's wallet balance"
method: POST
path: "/experiences/{experienceUuid}/action-purchases"
---

# Charge one action from the fan's wallet balance

`POST /experiences/{experienceUuid}/action-purchases`

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.

## Path parameters

- `experienceUuid` string, uuid, required

## Headers

- `X-Fanvue-API-Version` string, required
- `Idempotency-Key` string, required

## Request body

- object
  - `externalActionId` string, 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.
  - `fanUuid` string, uuid, required — The fan to charge, as returned by the token exchange. They must have given spend consent for this experience.
  - `clientReferenceId` string — Your own reference for this charge, carried through the purchase and echoed on the settled-purchase webhook.

## Response `201`

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

- object
  - `purchaseReference` string, 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`.
  - `experienceUuid` string, uuid, required — The experience the action belongs to.
  - `externalActionId` string, required — The app's own stable key for the action that was bought.
  - `clientReferenceId` string, nullable, required — The app-supplied reference carried through the purchase, or null when the app supplied none.
  - `buyerUuid` string, 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.
  - `amount` integer, required — Price in minor currency units (cents), frozen at the purchase attempt. A later re-pricing of the action does not move it.
  - `currency` string, 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.
  - `invoiceNumber` string, required — Fanvue invoice number for the charge. For support and accounting, not fulfilment.
  - `createdAt` string, date-time, required — When the purchase attempt was opened.
  - `paidAt` string, date-time, nullable, required — When the charge settled, or null while it has not.
  - `walletBalance` integer, required — The fan's remaining wallet balance in minor units (cents, USD) after this charge.

## Other responses

- `400` — Bad Request - API version not supported OR validation failed OR the Idempotency-Key header is missing
- `401` — Unauthorized Response
- `402` — The fan's wallet balance does not cover the action. Nothing was charged and no purchase attempt was recorded.
- `403` — The experience does not belong to the calling app, the fan has given no spend consent for it (`error: "consent_required"`), or the fan cannot be charged (`error: "payer_ineligible"`: account not active, payments blocked, blocked by the creator, or the creator requires age verification the fan has not passed)
- `404` — Experience not found, not owned by the calling app, or no such action in it
- `409` — A charge with this Idempotency-Key is still running, or the key was already used for a different charge
- `410` — API version no longer supported (sunset)
- `429` — Too many requests - rate limit exceeded
- `502` — The charge could not be completed against the upstream
- `503` — Wallet charges are not enabled for this creator, or the developer API upstream is not configured

## Changes

- **2026-09-25** `431868e8a264` — 1 info
  - endpoint added

[Change history](https://skmtc.dev/fanvue/apis/fanvue-api/changes/experiences/:experienceUuid/action-purchases/post.md)

---

[API](https://skmtc.dev/fanvue/apis/fanvue-api.md) · [All operations](https://skmtc.dev/fanvue/apis/fanvue-api/llms.txt) · [OpenAPI document](https://skmtc.dev/fanvue/apis/fanvue-api/revisions/431868e8a264?raw)
