Revenue API

Push customer, charge, and subscription data from any payment provider into Himetrica. Manual revenue data integrates seamlessly with all existing revenue metrics — MRR, timeline, cohorts, top customers, and visitor linking.

Overview

The Revenue API lets you track revenue from payment providers that Himetrica doesn't natively integrate with (beyond Stripe, Shopify, and AbacatePay). Use it for:

  • Custom payment gateways or in-house billing
  • Offline or invoice-based payments
  • Marketplace payouts and platform fees
  • Webhook-driven revenue ingestion from any source

Base URL: https://app.himetrica.com/api/v1

How it works

A "manual" integration is automatically created for your project on the first API call. Customers, charges, and subscriptions are stored in the same tables as native integrations, so all dashboard charts and metrics include manual data with zero additional configuration.

Amounts are in currency units, not cents

Every amount in this API is a decimal in the currency's main unit: 99.00 means $99.00, and 1623.50 with currency: "brl" means R$ 1,623.50. Do not send minor units. If your billing system stores cents, divide by 100 before sending.

Authentication

All Revenue API endpoints require a secret key passed in the X-API-Key header. Secret keys start with hm_sk_ and are the same keys used by the Server API.

Keep your secret key safe

Never expose the secret key in client-side code, public repositories, or browser-accessible files. Use environment variables on your server.

Rate Limiting

All Revenue API endpoints share a single rate limit:

ScopeLimitWindow
All /revenue/* endpoints500 requests1 minute

Rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset) are included in every response.

Customers

Two kinds of ID

The :id in these paths is the Himetrica customer id (a UUID we return), not your externalId. Resolve one from the other with GET /revenue/customers?externalId=..., or skip the lookup entirely by using the batch endpoint, which addresses everything by your own IDs.

POST/revenue/customers

Create a customer, or update the existing one. Matching follows the same rules as the batch endpoint: by externalId first, then by email. Retries and re-syncs never create duplicates. Returns 201 when created, 200 when an existing customer was updated. If a visitor with the same email exists in the project, they are automatically linked.

Request Body

email (required) — Customer email

name (optional) — Customer name

currency (optional) — Default currency (e.g. usd, eur). Defaults to usd.

externalId (optional) — Your system's customer ID

metadata (optional) — Arbitrary key-value pairs

bash
curl -X POST "https://app.himetrica.com/api/v1/revenue/customers" \
  -H "X-API-Key: hm_sk_your_secret_key" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "jane@example.com",
    "name": "Jane Doe",
    "currency": "usd"
  }'

Response 201

json
{
  "id": "a1b2c3d4-...",
  "externalCustomerId": "manual_f8e2a1b3c4d5",
  "email": "jane@example.com",
  "name": "Jane Doe",
  "currency": "usd",
  "totalRevenue": 0,
  "totalRevenueUsd": 0,
  "activeSubscriptions": 0,
  "subscriptions": [],
  "createdAt": "2025-03-19T12:00:00.000Z",
  "updatedAt": "2025-03-19T12:00:00.000Z"
}

GET/revenue/customers

List manual customers with pagination and optional search.

Query Parameters

page — Page number (default: 1)

limit — Items per page (default: 50, max: 100)

search — Fuzzy filter by email or name

externalId — Exact match on your own customer ID. Use this to resolve a customer's Himetrica id without paging. Takes precedence over search.

Each row includes externalCustomerId, totals, and the full subscriptions array, which makes this the cheapest way for a sync job to reconcile subscription state. Charges are not included here; fetch them per customer with GET /revenue/customers/:id.

bash
# Paginated list
curl "https://app.himetrica.com/api/v1/revenue/customers?page=1&limit=20&search=jane" \
  -H "X-API-Key: hm_sk_your_secret_key"

# Exact lookup by your own customer ID
curl "https://app.himetrica.com/api/v1/revenue/customers?externalId=cus_123" \
  -H "X-API-Key: hm_sk_your_secret_key"

GET/revenue/customers/:id

Get a single customer with their subscriptions and charge history.

bash
curl "https://app.himetrica.com/api/v1/revenue/customers/{id}" \
  -H "X-API-Key: hm_sk_your_secret_key"

PUT/revenue/customers/:id

Update a customer's fields. Only include the fields you want to change.

bash
curl -X PUT "https://app.himetrica.com/api/v1/revenue/customers/{id}" \
  -H "X-API-Key: hm_sk_your_secret_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Jane Smith",
    "metadata": { "plan": "enterprise" }
  }'

DELETE/revenue/customers/:id

Delete a customer and all their associated charges. This action is irreversible.

There is no bulk delete or project-wide reset in the API. To start over, delete the manual integration from your project's Integrations tab: that removes every manual customer, charge, and subscription, along with the stored MRR history. The integration is recreated automatically on your next API call.

bash
curl -X DELETE "https://app.himetrica.com/api/v1/revenue/customers/{id}" \
  -H "X-API-Key: hm_sk_your_secret_key"

Charges

Charges represent one-time payments or invoice line items. They update the customer's total revenue and appear in the revenue timeline chart.

POST/revenue/customers/:id/charges

Add a charge to a customer. The customer's total revenue is automatically recalculated.

Request Body

amount (required) — Charge amount in currency units, not cents (99.00 = $99.00). Stored with 2 decimal places.

currency (optional) — ISO currency code. Defaults to the customer's currency.

status (optional)succeeded (default), pending, failed, refunded. Only succeeded counts toward revenue totals and the timeline; the other statuses are stored but excluded.

description (optional) — Human-readable description

paymentMethod (optional) — e.g. card, bank_transfer, pix

cardBrand (optional) — e.g. visa, mastercard

chargeDate (optional) — ISO 8601 timestamp. Defaults to now. Always send it when backfilling history, and get it right the first time: re-sending the same externalId does not change it.

externalId (optional) — Your system's charge/invoice ID. Used for idempotency.

bash
curl -X POST "https://app.himetrica.com/api/v1/revenue/customers/{id}/charges" \
  -H "X-API-Key: hm_sk_your_secret_key" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 99.00,
    "currency": "usd",
    "description": "Pro Plan - March 2025",
    "externalId": "inv_abc123"
  }'

Response 201

json
{
  "externalId": "inv_abc123",
  "amount": 99,
  "amountUsd": 99,
  "currency": "usd",
  "status": "succeeded",
  "description": "Pro Plan - March 2025",
  "createdAt": "2025-03-19T12:00:00.000Z"
}

DELETE/revenue/charges/:id

Delete a charge. The customer's total revenue is automatically recalculated.

bash
curl -X DELETE "https://app.himetrica.com/api/v1/revenue/charges/{chargeId}" \
  -H "X-API-Key: hm_sk_your_secret_key"

Subscriptions

Subscriptions represent recurring revenue and are used to calculate MRR. They are stored on the customer object and update the MRR charts in your dashboard.

How MRR is calculated

MRR is normalized to a month: amount ÷ months in the billing cycle, summed across every subscription with status: "active". Send the full cycle amount, not a monthly equivalent: a $1,200 annual plan is amount: 1200, interval: "year" and contributes $100 of MRR. ARR is MRR × 12. trialing and past_due are excluded from MRR (trials still feed the trial funnel and conversion rate). Two subscriptions left active on the same customer are counted twice, so cancel the old one in the same payload when a customer changes plan.

Subscriptions drive MRR only. Actual money collected comes from charges, so sending both does not double-count revenue.

POST/revenue/customers/:id/subscriptions

Add a subscription to a customer. Duplicates are avoided by default: if you send an externalId that already exists on the customer, that subscription is updated in place. Without an externalId, an existing active or trialing subscription with the same productName is updated instead. To intentionally add a second subscription for the same customer, send forceNew: true. Returns 201 when a subscription was created, 200 when an existing one was updated.

Request Body

productName (required) — Name of the plan/product

amount (required) — Recurring amount charged per full billing cycle, in currency units, not cents (29.00 = $29.00)

currency (optional) — ISO currency code. Defaults to customer's currency.

interval (optional)day, week, month (default), or year

intervalCount (optional) — Positive integer multiplier on interval. Defaults to 1. Examples: quarterly = month × 3, semestral = month × 6, biennial = year × 2.

status (optional)active (default), trialing, canceled, past_due

externalId (optional) — Your system's subscription ID

forceNew (optional) — Set to true to always create a new subscription, skipping the same-productName matching. Ignored when externalId is provided.

startDate (optional) — ISO 8601 date the subscription began in your system. Send it when backfilling history: without it the historical MRR chart falls back to the ingestion timestamp, which lands every imported subscription in the month of your first sync.

currentPeriodStart / currentPeriodEnd (optional) — ISO 8601 dates

canceledAt (optional) — When the subscription was canceled. Send it together with status: "canceled" to take the subscription out of MRR.

trialStart / trialEnd (optional) — Trial period dates. A trial converts when the same subscription (same externalId, keeping trialEnd) flips from trialing to active. If you create a brand new record on conversion, copy trialStart/trialEnd onto it or the conversion is not counted.

bash
curl -X POST "https://app.himetrica.com/api/v1/revenue/customers/{id}/subscriptions" \
  -H "X-API-Key: hm_sk_your_secret_key" \
  -H "Content-Type: application/json" \
  -d '{
    "productName": "Pro Plan",
    "amount": 29.00,
    "currency": "usd",
    "interval": "month",
    "status": "active"
  }'

Response 201

json
{
  "externalId": "manual_sub_d4e5f6a7b8c9",
  "productName": "Pro Plan",
  "status": "active",
  "amount": 29,
  "amountUsd": 29,
  "currency": "usd",
  "interval": "month",
  "createdAt": "2025-03-19T12:00:00.000Z"
}

PUT/revenue/customers/:id/subscriptions/:subId

Update a subscription. Use this to change the plan, cancel, or update the status. The subId is the subscription's externalId.

bash
curl -X PUT "https://app.himetrica.com/api/v1/revenue/customers/{id}/subscriptions/{subId}" \
  -H "X-API-Key: hm_sk_your_secret_key" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "canceled",
    "canceledAt": "2025-03-19T00:00:00.000Z"
  }'

DELETE/revenue/customers/:id/subscriptions/:subId

Remove a subscription entirely. If you want to mark it as canceled instead, use the PUT endpoint with status: "canceled".

bash
curl -X DELETE "https://app.himetrica.com/api/v1/revenue/customers/{id}/subscriptions/{subId}" \
  -H "X-API-Key: hm_sk_your_secret_key"

Batch

Import or sync many customers, charges, and subscriptions in a single request. Ideal for backfilling historical revenue or running periodic syncs from your billing system without hitting the rate limit.

POST/revenue/batch

Upserts each customer and their nested charges and subscriptions. Customers are matched by externalId first, then by email — existing customers are updated, new ones are created. Charges and subscriptions are idempotent by externalId, so it is safe to retry the whole request or re-run a sync.

Request Body

customers (required) — Array of customer objects. Each accepts the same fields as POST /revenue/customers, plus:

charges (optional) — Array of charge objects (same fields as POST .../charges)

subscriptions (optional) — Array of subscription objects (same fields as POST .../subscriptions)

Limits

Up to 100 customers per request, and up to 1,000 total items (customers + charges + subscriptions).

bash
curl -X POST "https://app.himetrica.com/api/v1/revenue/batch" \
  -H "X-API-Key: hm_sk_your_secret_key" \
  -H "Content-Type: application/json" \
  -d '{
    "customers": [
      {
        "email": "jane@example.com",
        "name": "Jane Doe",
        "currency": "usd",
        "externalId": "cus_123",
        "charges": [
          { "amount": 99.00, "externalId": "inv_001", "chargeDate": "2025-02-01T00:00:00Z" },
          { "amount": 99.00, "externalId": "inv_002", "chargeDate": "2025-03-01T00:00:00Z" }
        ],
        "subscriptions": [
          {
            "externalId": "sub_123",
            "productName": "Pro Plan",
            "amount": 99.00,
            "interval": "month",
            "status": "active",
            "startDate": "2025-02-01T00:00:00Z",
            "currentPeriodEnd": "2025-04-01T00:00:00Z"
          }
        ]
      },
      {
        "email": "john@example.com",
        "charges": [{ "amount": 49.00, "externalId": "inv_003" }]
      }
    ]
  }'

Response 200

json
{
  "customersCreated": 1,
  "customersUpdated": 1,
  "chargesUpserted": 3,
  "subscriptionsUpserted": 1,
  "errors": []
}

Partial failures

Each customer is processed independently. If one entry fails validation, the rest of the batch still goes through, and the failure is reported in the errors array with the entry's index and email. If every entry fails, the response status is 400.

The unit of failure is the customer, not the nested item. One invalid charge aborts that customer's remaining charges and all of its subscriptions, and any charges already written before the error stay written. Retry the whole customer entry, not just the failed item.

Using batch as a recurring full sync

Re-sending your entire dataset on a schedule is a supported pattern and the easiest way to keep the dashboard self-correcting. Four things to know before you build it:

  • The upsert never deletes. Dropping a subscription or charge from the payload does not remove it, it just stops changing. A subscription that disappears from your sync stays active and keeps counting MRR forever. Cancellation has to be explicit: send the record with status: "canceled" and canceledAt.
  • Omitted subscription fields keep their previous value. Only productName and amount are required and always overwrite. Send the full desired state on every sync rather than a partial diff.
  • Customers are matched by externalId, then by email, and externalCustomerId is never rewritten after creation. Two of your customers sharing one email address will collapse into a single Himetrica customer.
  • Prefer a few large requests to many small ones. Each request ends with a project-wide revenue snapshot, so the per-request overhead is proportional to your whole customer base. Batches of a few hundred items, sent sequentially, are the sweet spot.

Idempotency

When you provide an externalId on a charge, the API uses a unique constraint to prevent duplicates. If a charge with the same externalId already exists, the existing record is updated instead of creating a duplicate.

This makes it safe to retry webhook deliveries or re-run import scripts without worrying about double-counting revenue.

Re-sending an existing charge updates amount, status, description, paymentMethod, and cardBrand. It does not update chargeDate or currency, which are fixed at creation. To correct either one, delete the charge and create it again.

Subscriptions are idempotent by externalId too, scoped to their customer, and every field is updatable, including status, canceledAt, amount, and the period dates. Sending the same subscription under a different customer creates a second copy rather than moving it.

Best practice

Always pass your payment provider's transaction ID as externalId when recording charges. If omitted, a random ID is generated and idempotency is not enforced.

Currency Conversion

All amounts are stored in the original currency and automatically converted to USD. Charges use the historical exchange rate of their chargeDate (ECB reference rates, available from 2023 onwards), so a past charge is converted at the rate of the day it happened and re-sending it produces the same USD value. Subscriptions use the current rate, since MRR is a present-value metric. The USD equivalent feeds aggregated metrics like MRR, total revenue, and top customers.

Supported currencies include all major ISO 4217 codes (EUR, GBP, BRL, JPY, etc.). Historical rates cover the ~30 currencies published by the European Central Bank; charges in other currencies fall back to the current rate, and charges dated before 2023 use the earliest available rate.

Billing in a single non-USD currency?

Set your project's display currency to Original in Project Settings. The dashboard then reads the native amounts exactly as you sent them, with no FX in the path.

Error Handling

All errors return a JSON object with an error field.

StatusMeaning
400Bad request — missing or invalid parameters
401Unauthorized — missing or invalid secret key
404Customer, charge, or subscription not found
429Rate limited — too many requests
500Internal server error
503Service temporarily unavailable

Examples

Node.js / TypeScript

typescript
const API_URL = "https://app.himetrica.com/api/v1";
const SECRET_KEY = process.env.HIMETRICA_SECRET_KEY; // hm_sk_...

// Create a customer
const customer = await fetch(`${API_URL}/revenue/customers`, {
  method: "POST",
  headers: {
    "X-API-Key": SECRET_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    email: "jane@example.com",
    name: "Jane Doe",
    currency: "usd",
  }),
}).then(r => r.json());

// Add a charge
await fetch(`${API_URL}/revenue/customers/${customer.id}/charges`, {
  method: "POST",
  headers: {
    "X-API-Key": SECRET_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    amount: 99.00, // currency units, not cents
    currency: "usd",
    description: "Pro Plan - March 2025",
    externalId: "inv_abc123", // prevents duplicates
  }),
});

// Add a subscription
await fetch(`${API_URL}/revenue/customers/${customer.id}/subscriptions`, {
  method: "POST",
  headers: {
    "X-API-Key": SECRET_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    productName: "Pro Plan",
    amount: 29.00,
    interval: "month",
  }),
});

Python

python
import requests
import os

API_URL = "https://app.himetrica.com/api/v1"
SECRET_KEY = os.environ["HIMETRICA_SECRET_KEY"]  # hm_sk_...
HEADERS = {"X-API-Key": SECRET_KEY, "Content-Type": "application/json"}

# Create a customer
customer = requests.post(
    f"{API_URL}/revenue/customers",
    headers=HEADERS,
    json={"email": "jane@example.com", "name": "Jane Doe", "currency": "usd"},
).json()

# Add a charge
requests.post(
    f"{API_URL}/revenue/customers/{customer['id']}/charges",
    headers=HEADERS,
    json={
        "amount": 99.00,  # currency units, not cents
        "currency": "usd",
        "description": "Pro Plan - March 2025",
        "externalId": "inv_abc123",
    },
)

# Add a subscription
requests.post(
    f"{API_URL}/revenue/customers/{customer['id']}/subscriptions",
    headers=HEADERS,
    json={"productName": "Pro Plan", "amount": 29.00, "interval": "month"},
)

Webhook Handler (Express)

A common pattern is to use the Revenue API inside webhook handlers from your payment provider. This example shows how to record payments from a custom gateway:

typescript
// Example: Webhook handler for a custom payment provider
app.post("/webhooks/payments", async (req, res) => {
  const event = req.body;

  if (event.type === "payment.succeeded") {
    // Find or create customer in Himetrica
    let customer;
    try {
      const list = await fetch(
        `${API_URL}/revenue/customers?search=${encodeURIComponent(event.customer_email)}`,
        { headers: { "X-API-Key": SECRET_KEY } }
      ).then(r => r.json());

      customer = list.customers[0];
    } catch {}

    if (!customer) {
      customer = await fetch(`${API_URL}/revenue/customers`, {
        method: "POST",
        headers: { "X-API-Key": SECRET_KEY, "Content-Type": "application/json" },
        body: JSON.stringify({
          email: event.customer_email,
          name: event.customer_name,
          externalId: event.customer_id,
        }),
      }).then(r => r.json());
    }

    // Record the charge (externalId prevents duplicates on retries)
    await fetch(`${API_URL}/revenue/customers/${customer.id}/charges`, {
      method: "POST",
      headers: { "X-API-Key": SECRET_KEY, "Content-Type": "application/json" },
      body: JSON.stringify({
        amount: event.amount,
        currency: event.currency,
        externalId: event.payment_id,
        description: event.description,
        chargeDate: event.created_at,
      }),
    });
  }

  res.json({ received: true });
});
Himetrica - Analytics That Actually Matter