# Generic revenue API (https://www.getfounderhq.com/docs/analytics/revenue/generic-revenue-api)

Send payments and refunds from any processor FounderHQ has no connection for.
One request per money event.

<Callout title="Before you start">
You need a [secret key](/analytics/getting-started) (`fhq_sk_XXXX`) that is scoped
to one brand and may write events. A publishable key is rejected. A key with no
brand is rejected.
</Callout>

## Endpoint

```text
POST https://i.getfounderhq.com/api/revenue
authorization: Bearer fhq_sk_XXXX
content-type: application/json
```

FounderHQ answers `202` as soon as the command is stored. Normalization,
matching, and attribution happen after that, so a slow ledger never slows your
checkout.

Using the Node SDK? `events.captureRevenue(command)` posts the same body and
retries for you.

## Example

```bash
curl -X POST https://i.getfounderhq.com/api/revenue \
  -H "authorization: Bearer fhq_sk_XXXX" \
  -H "content-type: application/json" \
  -d '{
    "idempotencyKey": "acme:payment:pay_ABC123",
    "transactionId": "pay_ABC123",
    "transactionRefType": "payment_intent",
    "kind": "payment",
    "amountMinor": "4900",
    "currency": "USD",
    "occurredAt": "2026-05-23T09:15:00.000Z",
    "paymentRail": "razorpay",
    "environment": "live",
    "customerEmail": "jane@acme.com",
    "checkoutVisitorId": "6b1f0d64-1f0e-4e5a-9a2a-6b0f1d8c2e77",
    "providerCustomerId": "cust_42",
    "providerSubscriptionId": "sub_42",
    "subscription": {
      "status": "active",
      "interval": "P1M",
      "currentPeriodEnd": "2026-06-23T09:15:00.000Z",
      "plan": "growth"
    }
  }'
```

## Response

```json
{
  "ok": true,
  "deliveryId": "DELIVERY_ID",
  "status": "QUEUED",
  "created": true
}
```

| Field | Meaning |
| --- | --- |
| `ok` | Always `true` on a `202`. |
| `deliveryId` | FounderHQ's ID for this command. Log it. |
| `status` | `QUEUED`, `PROCESSING`, `PROCESSED`, or `FAILED`. |
| `created` | `true` for a new command. `false` when you replayed one FounderHQ already had. |

## Idempotency

`idempotencyKey` identifies the delivery. Reuse the same key on every retry of
the same money event.

- Same key, same body: FounderHQ returns `202` with `created: false`. Nothing
  is counted twice.
- Same key, different body: FounderHQ returns `409`. Pick a new key, or send
  the body you sent the first time.
- Different key, same `transactionId`, `transactionRefType`, and `kind`:
  FounderHQ still counts the money once. The transaction is what dedupes the
  economics.

Keys are scoped to the API key you send them with.

## Fields

| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `idempotencyKey` | string, 1–512 | yes | Stable ID for this delivery. Retries reuse it. |
| `transactionId` | string, 1–512 | yes | The processor's transaction ID. FounderHQ dedupes the money by it. |
| `transactionRefType` | enum | no, default `transaction` | What `transactionId` points at: `payment_intent`, `charge`, `invoice_payment`, `refund`, `transaction`, `order`, `dispute`, `credit_note`. |
| `kind` | enum | no, default `payment` | `payment`, `refund`, `dispute_lost`, or `credit_note`. |
| `amountMinor` | integer, string or number | yes | Amount in minor units. `4900` is $49.00. Never `0`. Signed or unsigned: FounderHQ normalizes the sign from `kind`. |
| `currency` | 3 letters | yes | The charged currency, for example `USD`. |
| `taxMinor` | integer | no | Tax inside the amount. |
| `feeMinor` | integer | no | The processor's fee. |
| `settlementAmountMinor` | integer | no | What actually reached your bank. |
| `settlementCurrency` | 3 letters | no | The currency of that settlement. |
| `reportingCurrency` | 3 letters | no | The currency you want this payment reported in. |
| `fxRate` | decimal, string or number | no | The rate used to reach the reporting currency. |
| `occurredAt` | ISO 8601 datetime | no | When the money moved. Without it, FounderHQ uses the time it received the command, and marks the date as a fallback. Send it. |
| `providerRevisionAt` | ISO 8601 datetime | no | When the processor last revised this record. FounderHQ uses it to decide which correction is newer. |
| `providerRevisionKey` | string, 1–512 | no | Your ID for that revision. Defaults to the idempotency key. |
| `paymentRail` | string, 1–64 | no, default `generic` | The processor this money moved on, for example `razorpay`. |
| `merchantAccountId` | string, 1–256 | no | Your merchant or account ID with that processor. |
| `environment` | string, 1–32 | no, default `live` | The environment this money moved in. FounderHQ stores it on the payment, so test money stays labelled as test. |
| `originalTransactionId` | string, 1–512 | required for reversals | The payment this refund, lost dispute, or credit note cancels. |
| `originalRefType` | enum | no, default `transaction` | What `originalTransactionId` points at: `payment_intent`, `charge`, `invoice_payment`, `transaction`, `order`. |
| `checkoutVisitorId` | string, 1–400 | no | The `fhq_anonymous_id` your checkout captured. This is what ties the payment to a channel. |
| `checkoutAccountContext` | string, 1–2048 | no | The `fhq_account_context` token, when the buyer paid on behalf of an account. |
| `providerCustomerId` | string, 1–512 | no | The processor's customer ID. It binds renewals to the same contact. |
| `providerSubscriptionId` | string, 1–512 | no | The processor's subscription ID. Required when you send `subscription`. |
| `customerEmail` | email | no | The payer's email. FounderHQ uses it to find the contact when no visitor ID is present. |
| `historicalImport` | boolean | no, default `false` | Marks a backfilled payment. The amount counts, but with no touch it stays unmatched instead of counting as direct traffic. |
| `metadata` | object | no | Your own keys. Stored with the payment. |
| `attributionWindowDays` | `30`, `60`, `90`, or `180` | no, default `90` | How far back FounderHQ looks for the touches that earn this payment. |
| `subscription` | object | no | Recurring state. See below. |

### `subscription`

Send this block on a recurring payment and FounderHQ drives MRR from it, the
same way a connected provider does.

| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `status` | enum | no, default `active` | `trialing`, `active`, `past_due`, `paused`, `canceled`, or `expired`. |
| `priceMinor` | integer | no | The plan price in minor units. Defaults to `amountMinor` on a payment. |
| `interval` | ISO 8601 period, or `null` | no | `P1M` for monthly, `P1Y` for yearly. Without it, MRR is `0` and the period stays unknown. |
| `currentPeriodEnd` | ISO 8601 datetime, or `null` | no | When this paid period ends. |
| `quantity` | integer, 1–10000 | no, default `1` | Seats or units. |
| `plan` | string, 1–200, or `null` | no | Your plan name, for example `growth`. |

## Rules FounderHQ enforces

| Rule | Why |
| --- | --- |
| `amountMinor` must not be `0` | A zero amount is not a money event. |
| A refund, lost dispute, or credit note must carry `originalTransactionId` | A reversal has to say what it reverses. |
| A payment must not carry `originalTransactionId` | A payment reverses nothing. |
| `subscription` requires `providerSubscriptionId` | MRR needs a subscription to attach to. |

## Errors

| Status | Meaning |
| --- | --- |
| `400` | The body failed a rule. The message names the first field that failed. |
| `401` | The key is missing, wrong, or not a secret key. |
| `403` | The key is not scoped to a brand, or cannot write events. |
| `409` | This idempotency key already exists with a different body. |

## Verify

1. Send a test payment with `"environment": "test"`.
2. Confirm the response is `202` with `"created": true`.
3. Send the same body again. The response says `"created": false`.
4. In FounderHQ, open Revenue. The payment shows your amount, and the channel
   that brought the customer when you sent `checkoutVisitorId`.
