Billing

Entitlements

Gate features and meter usage with a single API call. Define features per product in the dashboard, then ask Merida the runtime question your app cares about: 'Is this customer allowed to do X right now?'

Why entitlements

A subscription tells you the customer paid. An entitlement tells you what they're allowed to do with it.

  • Boolean features: feature toggles per plan (web_access, priority_support).
  • Metered features: usage with a per-cycle allowance (175 calls/month) and optional overage pricing.
  • Credits: pre-paid balance that decrements as customers consume.

Without entitlements you'd build the counter table, the reset cron, the rollover math, and the "you hit your limit" code path yourself. With entitlements you call /check and /track, and Merida holds the state.

Concepts

Meterresource

A (event filter, aggregation) pair that turns raw events into a numeric reading. Identified by a stable slug. Example: a meter with event_name: "ai_call" and aggregation: "sum" over the total_tokens field.

Feature keystring

The stable identifier your app passes to /check and /track. Per-product allowances let "calls" mean 100 on Lite and 175 on Pro without forking the key.

Product entitlementresource

Attaches a feature to a product with allowance rules (FIXED/UNLIMITED/NONE), reset interval, optional rollover, and optional overage price. Configure under Dashboard → Products → (edit) → Entitlements.

Customer balancestate

Materialized when a subscription starts and re-credited on each cycle reset. Track decrements consumed, and check reads remaining.

Step 1: Define a meter

Meters are the source of truth for "how do I count this?". Create one per thing your customers consume: API calls, tokens, seats, GB.

bash
curl -X POST https://api.meridapay.com/meters \
  -H "Authorization: Bearer $MERIDA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "AI Calls",
    "slug": "calls",
    "aggregation": "sum",
    "event_name": "ai_call",
    "value_property": "tokens",
    "unit_label": "tokens"
  }'
slugstringrequired

Lowercase, alphanumeric + _-. Reuse this as your feature_key.

aggregationenumrequired

sum · count · max · last · unique_count. count ignores value_property.

event_namestringrequired

The name field on events that should feed this meter.

value_propertystring

Which metadata field carries the numeric value. Omit when events use the top-level value field.

filterobject

Optional metadata filter. { "model": "gpt-4" } means only events with metadata.model = "gpt-4" count.

Step 2: Attach the feature to your products

Open a product's edit page in the dashboard. Click Attach feature, pick the meter, and set the allowance:

  • Type: Boolean / Metered / Credit
  • Allowance: 175 (per period)
  • Reset: per billing cycle / monthly / daily / no reset
  • Rollover (optional): carry unused → next period, with optional cap
  • Overage price (optional): a product_price that bills consumption beyond allowance

You can attach the same feature to multiple products with different allowances: calls on the Lite product can grant 100 while the same calls key on Pro grants 175. Your app code stays a single check({ feature_key: "calls" }) call.

Or do it via API:

bash
curl -X POST https://api.meridapay.com/products/$PRODUCT_ID/entitlements \
  -H "Authorization: Bearer $MERIDA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "feature_key": "calls",
    "feature_type": "METERED",
    "meter_id": "<meter-uuid>",
    "allowance_type": "FIXED",
    "allowance": "175",
    "interval_type": "CYCLE",
    "rollover_enabled": true,
    "rollover_cap": "300",
    "overage_price_id": "<product-price-uuid>"
  }'

Step 3: Gate every request with /check

Call this on the hot path. One round trip returns the runtime answer plus enough context to render an upgrade CTA.

ts
const res = await fetch("https://api.meridapay.com/check", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.MERIDA_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    external_customer_id: orgId,
    feature_key: "calls",
    value: 1,                       // optional; used to project the next call
  }),
});

const { allowed, balance, limit, consumed, reason } = await res.json();
if (!allowed) {
  return new Response("Quota exceeded. Upgrade to keep going.", { status: 402 });
}

Response shape:

allowedboolean

true if the customer can use the feature. Boolean features return true when granted. Metered and credit return true when remaining ≥ value, or unconditionally when an overage_price_id is set.

balancenumber

Remaining units in the current period. null for booleans.

limitnumber

Total available this period (credited + rollover).

consumednumber

Used so far this period.

reasonenum

granted · within_allowance · overage · exhausted · no_active_entitlement · customer_not_found. Useful for analytics.

Step 4: Record consumption with /track

Fire-and-forget after a successful action. The feature key resolves to a meter (matching slugs) and the call is converted into one event.

ts
await fetch("https://api.meridapay.com/track", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.MERIDA_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    external_customer_id: orgId,
    feature_key: "calls",
    value: 1,
    idempotency_key: callId,        // optional; dedupes safely on retry
    metadata: { model: "gpt-4" },
  }),
});

Send the idempotency_key whenever the call sits behind a queue or webhook retry. Merida will silently treat duplicates as no-ops.

Bulk events: /events/ingest

When you control event aggregation yourself or want to backfill, post a batch of raw events. Each event matches every meter whose event_name and filter agree, and updates the corresponding customer_meters row.

bash
curl -X POST https://api.meridapay.com/events/ingest \
  -H "Authorization: Bearer $MERIDA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      {
        "name": "ai_call",
        "external_customer_id": "user_123",
        "value": 850,
        "metadata": { "model": "gpt-4" },
        "external_id": "msg_abc123"
      }
    ]
  }'

Up to 500 events per request. The response reports inserted and duplicates (matched on external_id).

Reading customer balances

For dashboards, "usage so far this month" widgets, or admin tools:

bash
curl https://api.meridapay.com/customers/$EXTERNAL_CUSTOMER_ID/balances \
  -H "Authorization: Bearer $MERIDA_API_KEY"

Returns one row per (customer, meter) for the current period:

json
{
  "data": [
    {
      "meter_id": "…",
      "meter_slug": "calls",
      "meter_name": "AI Calls",
      "unit_label": "tokens",
      "period_start": "2026-05-01T00:00:00Z",
      "period_end": "2026-06-01T00:00:00Z",
      "credited": 175000,
      "rollover": 0,
      "consumed": 42100,
      "balance": 132900,
      "is_exhausted": false
    }
  ]
}

Lifecycle: when balances appear and reset

Balances are projected onto the customer the moment a subscription becomes active, so there's no cold start where they show zero. Specifically:

  • On subscription create / checkout success: Merida walks the product's entitlements and writes a customer_meters row with credited = allowance and consumed = 0. The next /check returns the full quota.
  • On every cycle anniversary: an hourly cron rolls the period forward. consumed resets to zero, credited is reset to the allowance, and rollover carries any unused balance from the prior period (capped by rollover_cap when set).
  • On exhaustion: when consumed ≥ credited + rollover, a future webhook event will fire (entitlement.exhausted) so you can prompt the customer to upgrade.

Picking the right primitive

| You want | Use | | --- | --- | | "Does this customer have web access?" | Boolean feature | | "Has this customer used 175 calls this month?" | Metered feature | | "Top up the customer's wallet by 1,000 credits when they buy a credit pack" | Credit feature + a one-time top-up product whose entitlement grants the same feature_key | | "Bill any usage past 175 calls at $2.50 each" | Metered feature with overage_price_id set to a metered product price | | "I want a free tier with no quota, just a flag" | Boolean feature on the free product |

Errors

| Status | When | | --- | --- | | 401 | Missing or revoked API key | | 400 | Body validation failed (see error.issues) | | 404 | Product not found, or feature_key is not attached to any product in your org (catches typos in app code) | | 200 with allowed: false | Customer is real but exhausted or has no active subscription on a product that grants this feature |

The 404 vs 200 split is deliberate: a typo'd feature_key (saige_proo instead of saige_pro) surfaces immediately, instead of looking like every customer is unsubscribed.