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

# Record revenue

> Report sales and refunds from your backend with an organization API key

This walkthrough reports a sale and a refund to `POST /v1/revenue`, reads
the per-event results, and sets the reporting currency so they show up in
the console. Use it for revenue your modules don't already send - ticket
sales through the ticketing module are recorded at ingest. See
[Revenue](https://docs.fourdos.dev/concepts/revenue.md) for how totals work.

Call this endpoint from your server only. It takes an `org_` key, which is a
secret; the browser SDK and publishable keys cannot report revenue.

```bash theme={null}
export API_BASE_URL="https://api.develop.fourdos.dev"
```

## 1. Issue an organization API key

If your backend already has an `org_` key, skip ahead. Otherwise issue one
from a partner admin session (see
[Admin session tokens](https://docs.fourdos.dev/authentication.md#admin-session-tokens)). The
`raw_key` is returned only in this response.

```bash theme={null}
curl -X POST "$API_BASE_URL/v1/api-keys" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Club shop backend"}'
```

```bash theme={null}
export API_KEY="org_..."
```

## 2. Report a sale

Send a batch of up to 100 events. Amounts are positive integers in minor
units - `4500` is €45.00 - with an upper-case ISO 4217 currency.

```bash theme={null}
curl -X POST "$API_BASE_URL/v1/revenue" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      {
        "event_id": "shop-order-1042",
        "type": "sale",
        "amount_minor": 4500,
        "currency": "EUR",
        "category": "merch",
        "product_name": "Home shirt 26/27",
        "order_ref": "1042",
        "member_external_id": "cust-001",
        "occurred_at": "2026-10-04T14:12:00+02:00"
      }
    ]
  }'
```

| Field | Status | Notes |
| - | - | - |
| `event_id` | Required | Your id for this event, up to 128 characters. Reuse it on retries. |
| `type` | Optional | `sale` (default) or `refund` |
| `amount_minor` | Required | Positive integer minor units |
| `currency` | Required | Upper-case ISO 4217 code, e.g. `EUR` |
| `category` | Optional | Lowercase slug such as `ticket`, `merch`, `membership`; defaults to `other` |
| `product_name` | Optional | Shown in the console's recent events |
| `order_ref` | Optional | Your order reference, kept for reconciliation |
| `member_external_id` | Optional | Your id for the buyer. Send this or `user_id`, not both. |
| `user_id` | Optional | The platform user id of the buyer |
| `occurred_at` | Required | ISO 8601 timestamp with a timezone offset |

`202 Accepted`, with one result per event in request order:

```json theme={null}
{
  "results": [
    { "event_id": "shop-order-1042", "status": "accepted", "id": "…" }
  ]
}
```

## 3. Report a refund

A refund is its own event with a new `event_id` and a positive amount. A
partial refund just uses a smaller amount.

```bash theme={null}
curl -X POST "$API_BASE_URL/v1/revenue" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      {
        "event_id": "shop-refund-1042-1",
        "type": "refund",
        "amount_minor": 4500,
        "currency": "EUR",
        "category": "merch",
        "order_ref": "1042",
        "member_external_id": "cust-001",
        "occurred_at": "2026-10-05T09:30:00+02:00"
      }
    ]
  }'
```

## 4. Read the results

Each event comes back with one of three statuses:

* **`accepted`** - recorded; `id` is the stored revenue event.
* **`duplicate`** - this `event_id` was already recorded with the same type,
  amount and currency. Nothing changed, so a retried request is safe.
* **`rejected`** with `reason: "idempotency_conflict"` - this `event_id` was
  already used with different money. Nothing changed. Fix a wrong amount
  with a refund under a new id rather than resending.

A body that fails validation - a decimal amount, a lowercase currency, more
than 100 events, both `member_external_id` and `user_id` - is refused whole
with `422 VALIDATION_ERROR`, and nothing in it is recorded. Any credential
other than an `org_` key - an admin session included - is refused with
`401` or `403`. Each key can make 600 requests a minute; past that the API
answers `429`.

Things to know:

* **Unknown buyers are kept.** A `member_external_id` that isn't a member yet
  is stored as-is and linked automatically once a member with that
  `external_id` exists.
* **No XP.** Revenue reported here never awards XP. To reward a purchase,
  send an [earning event](https://docs.fourdos.dev/concepts/earning-events.md) as well.

## 5. Set the reporting currency

In the console, open **Setup › Settings** and pick the currency you report
in (administrators only). Totals only include events in that currency;
events in other currencies are kept and shown as not included. There is no
conversion between currencies.

## Where it shows up

With a currency set, the sale and refund above net to €0.00 with one order
and one paying member on **Operate › Revenue**. Revenue also appears on the
tenant dashboard, as lifetime spend on member detail and the members list,
on the Revenue dashboard card, and in AI analyst answers - for anyone holding the
**View revenue** capability. See [Revenue](https://docs.fourdos.dev/concepts/revenue.md#where-it-shows-up).


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