curl --request POST \
--url https://api.fanvue.com/v1/experiences/{experienceUuid}/action-purchases \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: <idempotency-key>' \
--header 'X-Fanvue-API-Version: <x-fanvue-api-version>' \
--data '
{
"externalActionId": "spin",
"fanUuid": "00000000-0000-4000-8000-000000000003"
}
'import requests
url = "https://api.fanvue.com/v1/experiences/{experienceUuid}/action-purchases"
payload = {
"externalActionId": "spin",
"fanUuid": "00000000-0000-4000-8000-000000000003"
}
headers = {
"X-Fanvue-API-Version": "<x-fanvue-api-version>",
"Idempotency-Key": "<idempotency-key>",
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {
'X-Fanvue-API-Version': '<x-fanvue-api-version>',
'Idempotency-Key': '<idempotency-key>',
Authorization: 'Bearer <token>',
'Content-Type': 'application/json'
},
body: JSON.stringify({externalActionId: 'spin', fanUuid: '00000000-0000-4000-8000-000000000003'})
};
fetch('https://api.fanvue.com/v1/experiences/{experienceUuid}/action-purchases', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));{
"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",
"status": "succeeded",
"invoiceNumber": "FVE-20260904-1234",
"createdAt": "2026-09-04T10:00:00.000Z",
"paidAt": "2026-09-04T10:00:02.000Z",
"walletBalance": 1800
}{
"message": "<string>"
}{
"error": "<string>"
}{
"error": "<string>",
"balance": 123,
"amount": 123,
"message": "<string>"
}{
"error": "<string>",
"message": "<string>"
}{
"message": "<string>"
}{
"message": "<string>"
}{
"error": "<string>",
"message": "<string>",
"nextVersion": "<string>"
}{
"error": "<string>"
}{
"message": "<string>"
}{
"message": "<string>"
}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.
curl --request POST \
--url https://api.fanvue.com/v1/experiences/{experienceUuid}/action-purchases \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: <idempotency-key>' \
--header 'X-Fanvue-API-Version: <x-fanvue-api-version>' \
--data '
{
"externalActionId": "spin",
"fanUuid": "00000000-0000-4000-8000-000000000003"
}
'import requests
url = "https://api.fanvue.com/v1/experiences/{experienceUuid}/action-purchases"
payload = {
"externalActionId": "spin",
"fanUuid": "00000000-0000-4000-8000-000000000003"
}
headers = {
"X-Fanvue-API-Version": "<x-fanvue-api-version>",
"Idempotency-Key": "<idempotency-key>",
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {
'X-Fanvue-API-Version': '<x-fanvue-api-version>',
'Idempotency-Key': '<idempotency-key>',
Authorization: 'Bearer <token>',
'Content-Type': 'application/json'
},
body: JSON.stringify({externalActionId: 'spin', fanUuid: '00000000-0000-4000-8000-000000000003'})
};
fetch('https://api.fanvue.com/v1/experiences/{experienceUuid}/action-purchases', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));{
"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",
"status": "succeeded",
"invoiceNumber": "FVE-20260904-1234",
"createdAt": "2026-09-04T10:00:00.000Z",
"paidAt": "2026-09-04T10:00:02.000Z",
"walletBalance": 1800
}{
"message": "<string>"
}{
"error": "<string>"
}{
"error": "<string>",
"balance": 123,
"amount": 123,
"message": "<string>"
}{
"error": "<string>",
"message": "<string>"
}{
"message": "<string>"
}{
"message": "<string>"
}{
"error": "<string>",
"message": "<string>",
"nextVersion": "<string>"
}{
"error": "<string>"
}{
"message": "<string>"
}{
"message": "<string>"
}write:experience— Request that the creator publish or unpublish a fan-facing experience. The app mints a request token; the creator confirms and Fanvue performs the change.
Authorizations
OAuth 2.0 access token, presented as a JWT bearer token in the Authorization header. Obtain a token via the authorization-code flow; the scopes granted to the token determine which operations it may call.
Headers
API version to use for the request
"2025-06-26"
Opaque key that makes this charge safe to retry. Required: the charge moves money, so a retry without one could debit the fan twice.
255Path Parameters
"00000000-0000-4000-8000-000000000010"
Body
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.
1 - 255"spin"
The fan to charge, as returned by the token exchange. They must have given spend consent for this experience.
"00000000-0000-4000-8000-000000000003"
Your own reference for this charge, carried through the purchase and echoed on the settled-purchase webhook.
200"round-42"
Response
The settled purchase, and the fan's remaining wallet balance
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.
"expact_5c9a1f0e-9f3f-4c2a-8f31-1d0f6a2b7c44"
The experience the action belongs to.
"00000000-0000-4000-8000-000000000010"
The app's own stable key for the action that was bought.
"spin"
The app-supplied reference carried through the purchase, or null when the app supplied none.
"round-42"
UUID of the fan who bought the action. The only identifier of the buyer this surface carries — no handle, no display name, no email.
"00000000-0000-4000-8000-000000000003"
Price in minor currency units (cents), frozen at the purchase attempt. A later re-pricing of the action does not move it.
700
ISO 4217 code of the amount. USD only today.
"USD"
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.
pending, succeeded, failed, refunded, disputed, cancelled Fanvue invoice number for the charge. For support and accounting, not fulfilment.
"FVE-20260904-1234"
When the purchase attempt was opened.
"2026-09-04T10:00:00.000Z"
When the charge settled, or null while it has not.
"2026-09-04T10:00:02.000Z"
The fan's remaining wallet balance in minor units (cents, USD) after this charge.
1800
Was this page helpful?