> ## Documentation Index
> Fetch the complete documentation index at: https://api.fanvue.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Experience action events

> Payload reference for the six app.experience.action.* events on priced actions inside your experiences, and the rule for when to fulfil.

Six events tell your app what happened to a priced-action charge inside one of its experiences. Fulfil only on `payment.succeeded`, and key everything on `purchase_reference`. [Payments inside experiences](/docs/app-store/experiences/payments) covers how priced actions are declared and charged.

| Event (`type`) | Fires when | `data.object` |
| - | - | - |
| `app.experience.action.payment.pending` | A fan's charge for a priced action is attempted | `experience_action_payment` |
| `app.experience.action.payment.succeeded` | The charge settles. The only event to fulfil on | `experience_action_payment` |
| `app.experience.action.payment.failed` | The charge is declined | `experience_action_payment` |
| `app.experience.action.refund.created` | A settled purchase is reversed: refund, chargeback or cancellation | `experience_action_refund` |
| `app.experience.action.dispute.flagged` | An early chargeback warning is raised on a settled purchase | `experience_action_dispute` |
| `app.experience.action.dispute.created` | A formal chargeback is opened on a settled purchase | `experience_action_dispute` |

The events are emitted for creators who have priced actions enabled. Every event requires `read:self` and reaches your app's destinations only, so register the topics in the **Events** tab or in `webhooks.destinations` of your App Manifest. Each arrives in the [envelope](/docs/webhooks/index#event-envelope), and the tables below describe `data`.

The fan is the buyer, the creator is the seller, and your app fulfils the action. These resources are distinct from `app.payment.*`, which describes your app selling its own plans to a creator, so a handler reconciling your own sales never has to filter them out.

## Fulfilment rule

Grant the action once per `purchase_reference`, and only after `app.experience.action.payment.succeeded` for that reference or a `succeeded` status on `GET /experiences/{uuid}/action-purchases`. `pending` describes a charge that may never land and `failed` one that did not. The bridge message your experience receives is interface state, not proof of payment. Granting on anything else gives the action away.

Only `payment.succeeded` has durable retry behind it, because a settled charge whose event could not be handed over is replayed from the ledger. The other five are best effort, and the purchases read is their backstop.

| `GET /experiences/{uuid}/action-purchases` status | Action |
| - | - |
| `pending` | Wait |
| `succeeded` | Fulfil once per `purchase_reference` |
| `failed` | Nothing to fulfil |
| `refunded`, `disputed`, `cancelled` | Revoke the grant made against that `purchase_reference` |

The read endpoint's status set differs from the webhook `status`. The webhook payment resource carries only `pending`, `succeeded` and `failed`, and reversals arrive as their own events. An open dispute that has not been lost still reads `succeeded`, so watch `app.experience.action.dispute.*` for that, not the read.

Amounts are integers in USD cents. `metadata` is the `client_metadata` object captured with the purchase, with string keys and values, and `{}` when none was set.

## Payment resource

`data.object` is `"experience_action_payment"`. `pending` and `failed` carry the same body as `succeeded`.

| Field | Type | Nullable | Notes |
| - | - | - | - |
| `object` | string | no | Always `"experience_action_payment"` |
| `id` | string | no | Fanvue invoice number |
| `status` | string | no | `pending` \| `succeeded` \| `failed`. Only `succeeded` is money that moved |
| `purchase_reference` | string | no | `expact_` reference. The fulfilment idempotency key |
| `gross` | integer | no | Gross amount in USD minor units |
| `currency` | string | no | Always `"USD"` |
| `external_action_id` | string | no | The action id your experience declared |
| `client_reference_id` | string | yes | Your attribution reference, when passed with the purchase |
| `metadata` | object | no | Echoed `client_metadata`, `{}` when none |
| `created_at` | string | yes | ISO 8601 time the charge was created |
| `paid_at` | string | yes | ISO 8601 time the charge settled. `null` until the charge settles |
| `experience` | object | no | `{ uuid }` of the experience |
| `app` | object | no | `{ uuid }` of your app |
| `buyer` | object | no | `{ uuid }` of the fan |

### Example: `app.experience.action.payment.succeeded`

```json theme={null}
{
  "id": "6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b",
  "type": "app.experience.action.payment.succeeded",
  "timestamp": "2026-08-02T18:04:11.420Z",
  "data": {
    "object": "experience_action_payment",
    "id": "INV-2026-004410",
    "status": "succeeded",
    "purchase_reference": "expact_5c9a1f0e-9f3f-4c2a-8f31-1d0f6a2b7c44",
    "gross": 700,
    "currency": "USD",
    "external_action_id": "spin",
    "client_reference_id": null,
    "metadata": {},
    "created_at": "2026-08-02T18:04:09.000Z",
    "paid_at": "2026-08-02T18:04:11.110Z",
    "experience": { "uuid": "d8f1a2b3-4c5d-4e6f-8a9b-0c1d2e3f4a5b" },
    "app": { "uuid": "a1c3e5f7-9b2d-4c6e-8a0f-2d4b6c8e0a13" },
    "buyer": { "uuid": "e2f3a4b5-6c7d-4e8f-9a0b-1c2d3e4f5a6b" }
  }
}
```

`pending` and `failed` deliveries differ in `status`, in `paid_at` being `null` and in the event `id`.

## Refund resource

`data.object` is `"experience_action_refund"`. The resource carries the same `purchase_reference` as the `succeeded` event it reverses, so you revoke exactly the grant you made against that key. It carries `amount` rather than `gross`, because a reversal reports what went back, not what the sale was worth.

| Field | Type | Nullable | Notes |
| - | - | - | - |
| `object` | string | no | Always `"experience_action_refund"` |
| `id` | string | no | Invoice number of the reversal |
| `payment_id` | string | no | Invoice number of the reversed payment, equal to the `id` of the `succeeded` event |
| `amount` | integer | no | Reversed gross in USD minor units, positive |
| `currency` | string | no | Always `"USD"` |
| `reason` | string | no | `refund` \| `chargeback` \| `cancel` |
| `purchase_reference` | string | no | Same `expact_` reference as the `succeeded` event |
| `external_action_id` | string | no | The action id your experience declared |
| `client_reference_id` | string | yes | Your attribution reference from the purchase |
| `metadata` | object | no | Echoed `client_metadata`, `{}` when none |
| `created_at` | string | yes | ISO 8601 time of the reversal |
| `experience` | object | no | `{ uuid }` of the experience |
| `app` | object | no | `{ uuid }` of your app |
| `buyer` | object | no | `{ uuid }` of the fan |

A refund and a later chargeback on one purchase are two events with two reversal invoice numbers and two event ids.

### Example: `app.experience.action.refund.created`

```json theme={null}
{
  "id": "7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c",
  "type": "app.experience.action.refund.created",
  "timestamp": "2026-08-05T10:20:33.000Z",
  "data": {
    "object": "experience_action_refund",
    "id": "INV-2026-004588",
    "payment_id": "INV-2026-004410",
    "amount": 700,
    "currency": "USD",
    "reason": "refund",
    "purchase_reference": "expact_5c9a1f0e-9f3f-4c2a-8f31-1d0f6a2b7c44",
    "external_action_id": "spin",
    "client_reference_id": null,
    "metadata": {},
    "created_at": "2026-08-05T10:20:31.000Z",
    "experience": { "uuid": "d8f1a2b3-4c5d-4e6f-8a9b-0c1d2e3f4a5b" },
    "app": { "uuid": "a1c3e5f7-9b2d-4c6e-8a0f-2d4b6c8e0a13" },
    "buyer": { "uuid": "e2f3a4b5-6c7d-4e8f-9a0b-1c2d3e4f5a6b" }
  }
}
```

## Dispute resource

`data.object` is `"experience_action_dispute"`. `flagged` is an early-warning alert and `created` a formal chargeback. Both are advisory. The money is still the creator's at `flagged`, and the reversal, when one follows, arrives as `app.experience.action.refund.created` with `reason: "chargeback"`.

| Field | Type | Nullable | Notes |
| - | - | - | - |
| `object` | string | no | Always `"experience_action_dispute"` |
| `id` | string | no | Processor dispute or alert id |
| `status` | string | no | `warning` on `dispute.flagged`, `open` on `dispute.created` |
| `amount` | integer | yes | Disputed amount in minor units |
| `currency` | string | yes | `"USD"` when present |
| `reason` | string | yes | Dispute reason, when the processor provides one |
| `created_at` | string | yes | ISO 8601 time the dispute was reported |
| `payment` | object | no | `{ id, transaction_id }`, both nullable; `id` is the original invoice number |
| `purchase_reference` | string | no | The `expact_` reference of the disputed purchase |
| `external_action_id` | string | no | The action id your experience declared |
| `client_reference_id` | string | yes | Your attribution reference from the purchase |
| `metadata` | object | no | Echoed `client_metadata`, `{}` when none |
| `experience` | object | no | `{ uuid }` of the experience |
| `app` | object | no | `{ uuid }` of your app |
| `buyer` | object | no | `{ uuid }` of the fan |

### Example: `app.experience.action.dispute.created`

```json theme={null}
{
  "id": "8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d",
  "type": "app.experience.action.dispute.created",
  "timestamp": "2026-08-09T07:15:02.000Z",
  "data": {
    "object": "experience_action_dispute",
    "id": "dp_2e9c4b7a1f3d",
    "status": "open",
    "amount": 700,
    "currency": "USD",
    "reason": null,
    "created_at": "2026-08-09T07:15:00.000Z",
    "payment": { "id": "INV-2026-004410", "transaction_id": "txn_9d1c4e7a" },
    "purchase_reference": "expact_5c9a1f0e-9f3f-4c2a-8f31-1d0f6a2b7c44",
    "external_action_id": "spin",
    "client_reference_id": null,
    "metadata": {},
    "experience": { "uuid": "d8f1a2b3-4c5d-4e6f-8a9b-0c1d2e3f4a5b" },
    "app": { "uuid": "a1c3e5f7-9b2d-4c6e-8a0f-2d4b6c8e0a13" },
    "buyer": { "uuid": "e2f3a4b5-6c7d-4e8f-9a0b-1c2d3e4f5a6b" }
  }
}
```

`app.experience.action.dispute.flagged` uses the identical shape with `"status": "warning"`.

## Deduplicating

The event `id` hashes your app, the event's own identifier and the topic. The identifier is the `purchase_reference` for a payment status, the reversal invoice number for a refund and the processor's dispute id for a dispute. A re-emit of one topic for the same purchase reuses the id, and the same purchase reaching two topics gets two ids. The id alone is a valid idempotency key on all six topics. See [Delivery, retries and idempotency](/docs/webhooks/delivery-and-idempotency).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.