Generic revenue API
Send revenue commands through the generic API.
Send payments and refunds from any processor FounderHQ has no connection for. One request per money event.
Before you start
You need a secret key (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.
Endpoint
POST https://i.getfounderhq.com/api/revenue
authorization: Bearer fhq_sk_XXXX
content-type: application/jsonFounderHQ 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 a payment or a
refund and retries for you. It needs a transactionId, so send
subscription state without a payment
with a direct request.
Example
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
{
"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
202withcreated: 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, andkind: FounderHQ still counts the money once. The transaction is what dedupes the economics.
Keys are scoped to the brand, not to the API key you send them with. Every secret key of a brand shares one idempotency space, one customer namespace, and one subscription namespace. Rotate a key and the next renewal still lands on the same customer and the same subscription.
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. Leave it out and FounderHQ keeps one namespace for the whole brand. Send it consistently, or not at all: FounderHQ dedupes transactions and subscriptions inside it. |
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, across every API key of the brand. |
providerSubscriptionId | string, 1–512 | no | The processor's subscription ID. Required when you send subscription. |
customerEmail | 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 or wrong. |
403 | The key is a publishable key, or your organization cannot use this API. |
429 | Too many requests. Read Retry-After and wait. |
403 | The key is not scoped to a brand, or cannot write events. |
409 | This idempotency key already exists with a different body. |
Verify
- Send a test payment with
"environment": "test". - Confirm the response is
202with"created": true. - Send the same body again. The response says
"created": false. - In FounderHQ, open Analytics → People and open the customer. The
payment is on their Revenue tab with your amount. When you sent
checkoutVisitorId, it also counts for the channel that brought them, in the Channel card of Analytics → Overview.
Subscription state without a payment
Send kind: "subscription" for a trial, pause, failed renewal, end, or confirmed
free service period. These commands do not create cash revenue. Do not send a
zero payment as a substitute.
{
"kind": "subscription",
"idempotencyKey": "acme:sub_42:free:2026-09",
"providerSubscriptionId": "sub_42",
"occurredAt": "2026-09-01T00:00:00.000Z",
"currency": "USD",
"status": "active",
"period": {
"kind": "free",
"startsAt": "2026-09-01T00:00:00.000Z",
"endsAt": "2026-10-01T00:00:00.000Z"
}
}For a state change without a confirmed free period, use
"period": { "kind": "lifecycle" }. Status can be trialing, active,
past_due, paused, canceled, or expired. Use canceled or expired only
when entitlement ends. A scheduled cancellation must keep active until then.
Use the same paymentRail, merchantAccountId, and environment as the
payments. A changed revision needs a new idempotency key. providerRevisionAt
and providerRevisionKey can record the provider's revision identity.
MRR stays zero before the first positive recurring payment. A confirmed free period after payment reduces MRR to zero; the next paid period increases it. Refund commands change cash and do not replace subscription MRR. A wallet top-up is cash only; do not describe it as recurring subscription service.
For a partial mid-period payment, set subscription.periodKind to proration.
It changes cash but retains the previous recurring baseline. The default is
full, for a full recurring service period. The first full positive payment
establishes MRR.