Documentation

What Metricaas reads from each gateway, what it assumes when the gateway does not record it, and how to pull every number back.

On this page

Guide

What the API does, how to authenticate, how to send your data, and what to expect when something goes wrong. Read it once, top to bottom, before your first call.

Getting started

Overview

One key, two things. Reading works for any account; sending is only needed when the data source is you.

  • READ your metrics: MRR, churn, LTV, revenue and 14 more, over the period and aggregation you ask for. Works for any source (Stripe, Asaas or custom data).
  • SEND your data: subscriptions, customers and payments, for those who do not use a gateway we integrate. In that case you can also type them in the app or upload a spreadsheet, under the same rules.
Base URL: https://api.metricaas.ai/v1

Everything is JSON, everything is authenticated by key, and dates are YYYY-MM-DD in your account timezone.

The endpoints

MethodEndpointWhat it is
GET/v1/metrics/{metric}Metric series
GET/v1/metricsMetrics catalog
POST/v1/subscriptionsCreate or update subscriptions
DEL/v1/subscriptions/{id}Delete a subscription
GET/v1/subscriptionsList subscriptions
POST/v1/customersCreate or update customers
DEL/v1/customers/{id}Delete a customer
GET/v1/customersList customers
POST/v1/paymentsCreate or update payments
DEL/v1/payments/{id}Delete a payment
GET/v1/paymentsList payments

Authentication

Every call carries your key in the header. The key belongs to your COMPANY (not to you), and each one carries only the permissions you checked when creating it.

Authorization: Bearer mtc_live_...
  • metrics:read - read metrics. This is what you need to pull your numbers.
  • data:write - send data. It only exists in accounts with a custom data source, and the key writes into that source.

We only store a cryptographic digest of the key, so not even we can show it again. Lost it? Create another and revoke the old one. Create keys in Settings > API.

Your first call

MRR for the last 12 months, month by month. Swap mrr for any metric in the catalog.

curl "https://api.metricaas.ai/v1/metrics/mrr?from=2026-01-01&to=2026-12-31&interval=month" \
  -H "Authorization: Bearer mtc_live_..."
{
  "metric": "mrr",
  "unit": "money",
  "aggregation": "point_in_time",
  "currency": "BRL",
  "timezone": "America/Sao_Paulo",
  "interval": "month",
  "from": "2026-01-01",
  "to": "2026-12-31",
  "points": [
    {
      "start": "2026-01-01",
      "end": "2026-02-01",
      "value": 1250000,
      "count": 42
    },
    {
      "start": "2026-02-01",
      "end": "2026-03-01",
      "value": 1310000,
      "count": 44
    }
  ]
}

It is the same number as the screen, and not out of discipline: by construction. The API calls the same engine the dashboard calls, asks for the same row, and does no math on top. There is no place where the two could diverge.

Reading the value

unit says how to read value: money is CENTS in the account currency (1250000 = $12,500.00), percent is a FRACTION (0.0312 = 3.12%), count is an integer.

aggregation says what the number MEANS. point_in_time is a snapshot at the end of the bucket (MRR, ARR, subscribers, LTV). sum_over_period is the sum of what happened in the bucket (revenue, trials). rate_over_period is a rate for the bucket (churn, growth).

Summing snapshots is the most common mistake when consuming SaaS metrics over an API. January MRR plus February MRR is not the MRR of the two months: it is a number that does not exist. Revenue, on the other hand, does add up. That is why aggregation ships in every response.

The calendar is YOUR ACCOUNT'S (the timezone you configured), never UTC. It is what makes "March" mean March to you.

Send your data

Only those who do NOT use an integrated gateway need this part. If your data comes from Stripe or Asaas, it already arrives on its own: skip to the Reference.

Getting started

In the custom data source, you are the one declaring the data. There is no gateway on the other side to check anything, so whatever you declare here is the truth every number in your account is computed from. If your system stops sending, your dashboard freezes at the last thing that arrived.

  1. In Metricaas, add a Custom data source and answer the three questions (whether you'll enter payments, whether each subscription has a plan, whether you'll register customers).
  2. Under Settings > API, generate a key. It is shown once.
  3. Send your subscriptions. The dashboard moves within a minute.

The three resources: one required, two optional

The subscription is the only thing we need. The other two are optional, and each one UNLOCKS a part of the product: you choose what you want to see, and pay for it by sending the matching data.

/v1/subscriptionsRequired

The heart of the source. This is where your MRR comes from -- with no subscriptions there is no metric to compute at all.

What it unlocks: MRR, ARR, churn, expansion, contraction, LTV, ARPA, active subscribers, cohorts. In short: nearly everything.

/v1/customersOptional

Optional. Only needed if one customer has more than one subscription -- with no customers, each subscription counts as one customer. The only required field is id, so you can group without telling us who they are.

What it unlocks: Grouping subscriptions by customer (ARPA and LTV become PER CUSTOMER, not per subscription), the Customers screen with name, email and phone, and search across all of them.

/v1/paymentsOptional

Optional. Without payments, MRR stays whole: it comes from the contract, not from the money that arrived.

What it unlocks: Collected revenue, processor fees, refunds, net revenue and DELINQUENCY -- which without payments simply does not exist in your account (there is no way to know who stopped paying).

You declare what you will send in the source settings (the three switches: payments, plans, customers). Declaring that you will send and then not sending is worse than not declaring: the dashboard waits for data that never arrives.

An amount is a HISTORY, not a number

In a gateway, price history lives in the invoices: each one is a billed period, at that day's price. Here there are no invoices, so the history lives inside the subscription, in the amounts field.

Each band says what it became worth, and from when. The first one starts when the subscription started. That is what keeps March's MRR being March's after you raise the price in April.

{
  "amounts": [
    {
      "from": "2024-01-10",
      "cents": 9900,
      "cycle": "monthly",
      "plan": "Pro"
    },
    {
      "from": "2024-06-10",
      "cents": 99000,
      "cycle": "yearly",
      "plan": "Pro anual"
    }
  ]
}

So a request that doesn't PRESERVE an already declared band is rejected with 409 (including inserting a band before another one that's already scheduled). If the affected band has already started, this rewrites the MRR of months that already closed. If it was a price change, append a new band. If it was a typo, confirm you really want to rewrite:

POST https://api.metricaas.ai/v1/subscriptions?correct_history=true

Canceling is not deleting

Canceling is a fact about your business: the customer left, the subscription stays in your history and becomes churn on the declared date. Deleting is an erratum: it never existed, and past MRR is recomputed without it. Confusing the two corrupts your churn in both directions.

# the customer left (becomes churn)
POST https://api.metricaas.ai/v1/subscriptions          { "id": "sub_001", "canceled_at": "2026-03-01", ... }

# I typed it wrong (erased from history)
DELETE https://api.metricaas.ai/v1/subscriptions/sub_001

Idempotency (optional)

You already have idempotency for free: the object id is yours. Sending the same subscription twice UPDATES it, it doesn't duplicate it. For most cases, that's all you need to know.

The header below covers one specific case: you wired our API into a webhook system of yours, and it can fire the same event twice at the same time. Send the same Idempotency-Key on both: the work happens once, and both requests get the same response.

Idempotency-Key: evt_7f3c9a21
  • Same key, same body again: you get the stored response back (same status, same JSON), with the header Idempotent-Replay: true. Nothing is reprocessed.
  • The first one is still running: the second gets 409 (in_progress). Retry in a few seconds to get its response.
  • The same key with a different body is refused with 400. One key is one request: use a new key for new content.
  • If the request failed, the key is released. Fix the body and send it again with the same key, as usual.

Errors and limits

Errors

A rejection lists ALL problems at once, with the field (and, in a spreadsheet, the line). You are not going to fix one per request.

{
  "error": {
    "code": "invalid_request",
    "message": "a declaracao tem 2 problema(s)",
    "details": [
      {
        "field": "subscription.cycle (sub_002)",
        "line": 4,
        "reason": "invalid option"
      },
      {
        "field": "subscription.amounts (sub_007)",
        "line": 9,
        "reason": "..."
      }
    ]
  }
}
  • 200 - success (reading metrics).
  • 202 - accepted (sending data). Your dashboard moves within a minute.
  • 400 - the request has problems: invalid parameter, or a rejected declaration. Nothing was written.
  • 401 - invalid key, revoked, or without permission for this operation.
  • 402 - the account plan does not allow reading metrics right now (MRR passed the free cap, or the contracted tier). Sort it out at metricaas.ai/billing.
  • 404 - the metric does not exist. Ask GET /v1/metrics for the full list.
  • 409 - this would rewrite value history already declared (or an identical request is in flight).
  • 429 - too many requests. Send in batches.

Limits

  • 120 requests per minute, per key. Every response carries X-RateLimit-* headers with what is left.
  • Up to 1000 objects per send (an array in the body). Either all of it lands, or none of it does.
  • Up to 1000 points per series. Above that the answer is a 400 asking for a larger interval: never a series cut in half, which reads like a business that stopped.
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118
X-RateLimit-Reset: 1781827200

Data sources

Where your numbers come from: what each gateway records, what it does not, and what we assume in its place.

Introduction

What a source is

Where your numbers come from. Each gateway has a section below with what it records, what it does not record, and the assumptions we make in its place. Not using an integrated gateway? The Custom data source covers the same ground, with you declaring the data instead of the gateway.

How the number is built

A source is where your numbers come from. The engine reads what the source recorded, translates it into Metricaas's internal model, and computes your metrics from that. Where the source does not record something, we do not invent it: either the metric disappears from the screen, or we make an assumption, and you have the right to read which one before trusting the number. What needs that decision from us is written in that source's own section.

Stripe

What Stripe knows

Your MRR is rebuilt from scratch out of what Stripe records. Where it records everything, we measure. Where it does not, either the metric disappears from the screen or we make an assumption, and you have the right to read which one before trusting the number. This section is generated from the adapter's own capability sheet, so it cannot go stale.

Each row is a fact Stripe records, or does not. A "no" is never just a "no": it is a metric that changes.

Product catalogYou can filter every metric by product.
Free trialTrials started, converted and the conversion rate are measured from the data, with no assumption of ours.
Free plan (freemium)A zero-value subscription is representable, and your free users show up.
Cancellation dateChurn is dated on the day the customer cancelled.
Event historyA missed webhook is recovered from the gateway's own event history.
Read-only keyThe key we ask for is restricted and read-only: it cannot move money.

Asaas

What Asaas knows

Your MRR is rebuilt from scratch out of what Asaas records. Where it records everything, we measure. Where it does not, either the metric disappears from the screen or we make an assumption, and you have the right to read which one before trusting the number. This section is generated from the adapter's own capability sheet, so it cannot go stale.

Each row is a fact Asaas records, or does not. A "no" is never just a "no": it is a metric that changes.

Product catalogProduct segmentation disappears for the whole account. The gateway has no products: a charge carries only a free-text description, and a filter covering half an account is worse than no filter at all, because it looks complete.
Free trialThe gateway has no such concept. Giving 7 free days and issuing the first charge 7 days from now are the same operation to it. If you really do offer a free trial, declare it in Calculation Premises and we will read that gap as a trial.
Free plan (freemium)The gateway refuses charges below R$5.00. A user on your free plan has neither a subscription nor a charge there: they exist only in your own database. There is nothing to read, and we do not invent what was never recorded.
Cancellation dateA deleted subscription does not say when it was deleted. Churn is dated at the end of the last paid period, which is the question the data can answer: not "when did they cancel?", but "how far had they paid?".
Event historyThere is no event history: a missed webhook is lost forever, and there is no one to ask for a redelivery. That is why the Data Sources screen warns you when delivery fails, and re-importing the source, which reads your whole account again, is the only net left.
Read-only keyThe gateway has no read-only key: every key it issues is full-access, so the key you hand us can withdraw funds. We only use it to read and to register our own webhook, and that is locked down by a test in our code, but the risk is yours and you have the right to know before pasting it. You can revoke it whenever you want in the Asaas dashboard, or create it with an expiry date (when it expires, syncing stops and the Sources screen tells you).

Why we ask

Your gateway cannot answer this. In it, giving 7 free days and issuing the first charge 7 days from now are exactly the same operation: no field tells the two apart. If we guessed from the gap between the subscription and its first charge, every subscription would become a trial (that gap always exists), and your funnel would show hundreds of trials that never happened. So we ask instead. Turn this on only if you really do give a free period before the first charge.

The switch lives in Settings, under Calculation Premises, and is off by default. Turning it on recalculates your history right away.

What Asaas does not provide

Each item below is data the gateway does not record. It is not a choice of ours, and reading more carefully will not work around it: the data does not exist on the other side.

Asaas has no product catalog

Asaas has no product catalog: a charge carries a free-text "description" field, typed in at issue time, and there is no product or price object for a subscription to point at. Two customers on the same plan can arrive with different descriptions, and nothing in the data says they are the same plan. That is why product segmentation is unavailable across the whole account while an Asaas source exists, and not only for the subscriptions that came from it: a filter that answered for half of your account and ignored the other half would look complete without being complete, and that is worse than no filter at all. Your MRR, your churn and your revenue do not change because of this; what disappears from Metrics is the product breakdown. There is no assumption to declare here: the data does not exist on the other side, and no amount of careful reading invents it.

Asaas records trials in no field at all

An Asaas subscription has not a single trial field: there is no "trialDays", no "trialEndDate", no equivalent, and sending any of them when creating a subscription is accepted with HTTP 200 and silently ignored (measured in the sandbox account). This is not a gap we can read our way around: to Asaas, giving 7 free days and issuing the first charge 7 days from now are the same operation, and that is how its own documentation tells you to run a trial. Not even Asaas can say who was on a trial. Consequence: in the Sales funnel, Trials Started sits at zero for this source, and Trial Conversion has nothing to measure. Your MRR does not change, because a subscription with no confirmed charge is already worth zero in our calculation: what is lost is the label, not the money. If you really do give a free period before the first charge, it only starts to exist once you declare the assumption in Settings, under Calculation Premises.

A free plan cannot be represented in Asaas

Asaas refuses any charge below R$ 5.00, and we measured it across the four payment methods: a value of zero returns 400 (to Asaas, zero reads as missing), one cent returns 400 ("the minimum value is R$ 5.00"), and R$ 100 with a 100% discount is refused too. This is not a limit of our reading: a user on your free plan has neither a subscription nor a charge in Asaas, they exist only in your own database. Consequence: in the Sales funnel, Freemiums Started sits at zero, Freemium Conversion has nothing to measure, and your free base does not show up in the Subscribers count. Your MRR does not change, because a free user brings no recurring revenue, but the top of your funnel looks smaller than it is in real life. No assumption fixes this: there is nothing to read, and we do not invent what was never recorded. If you need to measure your freemium, it would have to be declared through another source: the Custom data source accepts zero-value subscriptions.

Asaas has no event history

There is nothing in Asaas equivalent to Stripe's event history: you cannot list what happened, see the pending queue, or ask for a delivery to be sent again. If an event is lost, there is no one to ask what we missed. Consequence: the fact it carried, a payment or a cancellation, would stay out of your metrics, and nothing on the screen would give it away, because there is no second list to disagree with the first. Since we cannot ask, we raise alarms: the Data Sources screen tells you when the webhook was deleted or disabled, when Asaas paused the delivery queue after repeated failures (it holds paused events for 14 days, so resuming inside that window loses nothing; past it, they are gone for good), and when an event arrived and we failed to store it. Of those warnings, only the last one does not clear itself: the others leave the screen once the source is healthy again. A lost event does not come back, so when something was lost, the cure is to re-import the source, which reads your Asaas account again and rebuilds your history from what it answers. There is nothing to configure and nothing to declare: what we ask of you is to act on the warning when it shows up.

Asaas does not tell us when a customer changes

Asaas has no customer webhook: the four "CUSTOMER_*" events are refused with HTTP 400 when the webhook is created, while 24 others are accepted (we measured them one by one). A NEW customer still rides along with them, because the event for their subscription or their charge sends us to fetch them. What never reaches us is a CHANGE to someone already here: fixing a name, an email or a tax id in Asaas raises no event at all, and the new data only appears when you re-import the source. No metric changes because of this: MRR, churn and the Subscribers count do not depend on anyone's name, and what goes stale is the label you read under Customers. Re-checking every customer on every charge would fix it, and we do not do it: it would spend your account's API quota to correct data that moves no number. There is nothing to declare.

What we assume

Where the gateway has no answer, someone has to decide. These are our decisions, written down before you look at the number that comes out of them.

Trials only exist if you declare them

Since Asaas cannot tell "seven free days" apart from "first charge seven days from now", trials only exist if you DECLARE that your business has them, in Settings, under Calculation Premises. Without that, no customer is counted as a trial: it would be a guess, and a guess becomes a number on your screen. Once declared, the gap between the subscription being created and its first charge is read as a trial period, and the duration comes from the data itself, not from a number you type (which is why the question is yes or no, and not "how many days"). The side effect is stated plainly: if you also postpone the first charge for other reasons, a negotiation or a one-off deal, those subscriptions come in as trials and your trial metrics overstate. The assumption is off by default, moves only the Sales funnel and never the MRR, and turning it on or off recalculates your history right away.

Churn is dated at the end of the last paid period

A deleted Asaas subscription does not say when it was deleted: you get "dateCreated" and "nextDueDate", there is no "deletedDate", "canceledDate" or "endDate", and there is no event history to fish the date out of. Stamping the moment our sweep noticed the subscription was gone would be worse than having no date: the day of your churn would depend on when our robot ran, and reprocessing your history would move yesterday's chart. So we changed the question: "when did they cancel?" Asaas cannot answer, but "how far had they paid?" is recorded on every settled charge, and it stays there forever. The MRR span of an Asaas subscription is the union of its paid periods, and churn is dated at the end of the last one. In practice: someone who had paid through March 10 and did not renew shows up as churn on March 10, not on the day you deleted the subscription in the dashboard, so Customer Churn and MRR Churn follow the date in the data, not the date of your click. There is nothing to declare: this is the most faithful reading the Asaas data allows.

The biweekly cycle (BIWEEKLY) is treated as 14 days

Asaas has a cycle named "BIWEEKLY" and its documentation only calls it "quinzenal", an ambiguous word: in Portuguese it usually means 15 days (24 charges a year), in English biweekly means 14 (26 a year). We measured it by creating a real subscription in the sandbox account: the charges came out on July 14, July 28 and August 11, that is, 14 days. So the MRR of a biweekly subscription is its amount times 26, divided by 12. Guessing 15 days would take about 8% off the MRR of every subscription on that cycle, and the error would show up nowhere. If you do not use the biweekly cycle, this assumption touches none of your numbers, and there is nothing to declare.

A deleted customer is recovered through their charges

A deleted customer disappears from Asaas listings: "/v3/customers" has no "includeDeleted", so not even a full sweep of your base finds them. But they still answer by id (a direct GET returns HTTP 200), and their subscriptions and charges keep referencing that id. That is exactly what we use: when a subscription or a charge points at a customer the listing did not bring, we fetch that customer by id and bring them back. Without this recovery your history would lose people: in the real Asaas account we measured, 124 customers, 28% of the historical base, exist only because of it. Consequence: the Subscribers count, Customer Churn, ARPA and LTV also account for the people you have already deleted from Asaas, which is the right thing for a history, because their past did happen. There is nothing to declare.

What you need to know

Consequences of connecting this source that do not change your numbers, but do change what you should know before connecting.

Asaas has no read-only key

Every Asaas API key is full access, and there is no way to ask for less: the API specification has no "scope", no "permission" and no "readOnly", and the key creation body accepts only "name" and "expirationDate". This differs from Stripe, where we ask for a restricted key that, by construction, cannot move money: the key you paste here can issue charges, transfer money out of the account and delete customers. What we do with it is read, plus a single write: registering our own webhook. The HTTP client handed to the import, the drain and the sweep only knows how to GET, and the path that writes is locked to the "/webhooks" resource by a guard that a test keeps in place. Even so the risk is yours, and you have the right to know before pasting: you can revoke the key whenever you want in the Asaas dashboard, or create it with an expiry date up front (when it expires, syncing stops and the Data Sources screen tells you). None of this changes a number of yours: it is risk, not metric.

The Asaas API quota is yours, not ours

Asaas allows 25,000 requests every 12 hours per ACCOUNT, and that quota is shared with your own use of it: your site, your ERP and your checkout spend from the same pocket as our reading. If our sweep were the call that exhausted the quota, YOUR operation's calls would start failing, and that is unacceptable: a wrong metric is bad, freezing your cash flow is not negotiable. So every pass has a declared ceiling of 2,000 calls, about 8% of the 12-hour quota, and uses 6 concurrent calls even though Asaas accepts 50; when the ceiling is reached the sweep stops, says it stopped, and continues on the next pass, losing nothing. And we stop at the first rate-limit refusal, without insisting: Asaas returns no rate-limit header at all (we looked across some 55 requests and not one came back), so we cannot tell a burst limit apart from your quota running out, and in doubt your operation comes first. When that happens, the Data Sources screen raises the alarm. There is nothing to declare and nothing to configure.

Custom data

When you are the source

In this source there is no gateway to check anything against: you are the one declaring subscriptions, customers, and payments, through the API, the screen, or a spreadsheet. Three questions decide what Metricaas can compute from what you declare: plan, payments, and customers. Below, question by question, is what becomes unavailable if you leave it off and what it unlocks if you turn it on.

What always holds

In this source, you are the one reporting the data

In the other sources there is a gateway on the other side, and what it recorded is the reference everything is checked against. Here there is none: the subscriptions, the customers and the payments of this source are exactly the ones you declared, through the screen, a spreadsheet or the API. Consequence: what you declare is the truth, and every number in this account is computed from it. A subscription you forgot to declare shows up nowhere, and nothing gives it away, because there is no second copy to disagree with the first. For the same reason, this source holds the ONLY copy of this data: there is nowhere to re-import it from, removing the source erases what you declared and rebuilds your metrics without it, and that is why the screen asks for a backup first. There is no assumption to declare here: it is the nature of the source, and it is the same for someone declaring three subscriptions and for someone declaring three thousand.

The spreadsheet assumes one single amount per subscription

A spreadsheet row carries ONE amount; a subscription carries a HISTORY of amounts. There is no column that says "it cost R$ 99 until March and R$ 149 after that", so a spreadsheet can only claim one thing: this subscription was always worth this. That is what we assume when you upload the file, and the amount in the "amount" column applies from the subscription's start date onward. Consequence: if the price changed along the way, past months' MRR comes out at today's price, and the upgrade disappears from the month it happened in (in Metrics there will be no expansion or contraction there, and the chart rises as if it had always been that way). Uploading the spreadsheet again with the new price does not silently fix it: the whole upload is refused, because rewriting an amount you already declared changes the MRR of months that have closed, and that is an erratum, which you have to confirm. What to do: declare the subscription's price bands, on the screen or through the API, one band per price with the date it came into effect, and then each month uses that month's price and the upgrade shows up as expansion on the day it happened.

The three questions

I plan to report the plan of each subscription

If you leave it off

Without a declared plan, there is nothing to segment

Without a plan on each subscription, they arrive with no name for the product they are subscribed to. Consequence: product filters and breakdowns are unavailable for whatever came from this source, and in Metrics there is no way to answer "how much of my MRR comes from the Pro plan". No number changes value: MRR, churn and revenue are the same with or without a plan, what is missing is the cut. To get that cut, turn this answer on and send the plan name on each subscription (Basic, Pro, whatever you use): the recalculation is immediate, and the product filter starts to exist.

I plan to report the payments received

If you leave it off

Without declared payments, there is no dunning

Without payments, this source has the subscriptions but not the money that came in. So dunning does not exist: nobody shows up as overdue, and the tolerance ruler you chose under Calculation Premises does not apply to this source, it is decoration here. Cash does not exist either: in Metrics, Gross Revenue, Net Revenue, Gateway Fees and Refund Rate sit at zero for whatever came from this source. Your MRR stays whole, and so do ARR, Subscribers and churn: MRR measures what was contracted, not what was received, and with no declared payment every subscription is treated as current (inventing dunning here would erase the MRR of people who pay on time). To get the cash metrics, turn this answer on and declare the payments: they start to exist from what you declare onward, and the tolerance ruler applies again.

If you turn it on

A lag in entering payments becomes a lag on screen

With payments declared, this source's dunning comes from exactly what YOU entered: a payment that has not reached us yet did not happen, as far as we know. Consequence: a customer who paid on time shows up as overdue until you enter their payment, and if you only enter payments once a month, that is what the screen shows all month. And it does not stop at the label: past your company's tolerance ruler (under Calculation Premises) the subscription stops counting MRR, so your MRR and your Subscribers drop because of a data-entry lag, not because of churn. This is not a bug, ours or yours: it is the only possible reading of data that exists only when you declare it, and it is the same mechanism that makes the ruler actually work for people who really did stop paying. What to do: declare payments as often as you want dunning to be true. Raising the ruler buys time, but it applies to all of your customers, not only to the ones you have not entered yet.

I plan to report my customers' data

If you leave it off

Without declared customers, each subscription counts as one

Without customer data there is no way to know that two subscriptions belong to the same person, so we assume one subscription, one customer. Your MRR and your revenue do not change: they add up per subscription, and the sum is the same. What changes are the per-customer metrics: if one of your subscribers has two subscriptions they become two customers, so Subscribers goes up, ARPA goes down (the same revenue split across more people), LTV goes down with it, and one cancellation of theirs counts as two in Customer Churn. If each of your customers has at most one subscription, the assumption gets nothing wrong, and leaving this off is the right call: you do not have to reveal who they are. If one customer can have more than one, turn this answer on and send an identifier of yours to group them: the identifier alone is enough, name and email are optional.

Reference

The endpoints, one by one: what each takes, what it returns, and a curl that works pasted as is.

Metrics

The numbers, already computed in your account timezone and currency. They are the SAME ones your dashboard shows.

The series

GET/v1/metrics/{metric}

One point per bucket, over the period and time aggregation you ask for. All three parameters are optional: with none, you get the last 12 months, month by month.

Parameters

ParameterTypeDefaultWhat it is
fromAAAA-MM-DDto - 365dFirst day of the period. The bucket containing this date comes in whole.
toAAAA-MM-DDhojeLast day of the period. Defaults to today, in your account timezone.
intervalday | week | month | quarter | yearmonthThe bucket size. This is what turns "month by month" into "day by day".

The response

FieldWhat it is
metricThe metric you asked for, named as it is in the catalog.
unitmoney, percent or count. Says how to read value (see below).
aggregationpoint_in_time, sum_over_period or rate_over_period. Says what the number means.
currencyThe account currency, ISO-4217. Only meaningful when unit is money.
timezoneThe account timezone. It is the calendar the buckets were cut in.
intervalThe interval actually applied (what you asked for, or the default).
from, toThe period actually applied. Check here what the default picked for you.
points[]The series. Each point has start, end (EXCLUSIVE), value and, when it exists, count: how many items are behind the number. A metric with no count omits the field instead of sending zero.

Buckets are calendar-aligned: with interval=month, a from=2026-03-15 returns the bucket starting 2026-03-01. Every point carries its own start and end so you never have to derive them.

The metrics catalog

GET/v1/metrics

The live list of metrics, with your account currency and timezone. Ask this endpoint instead of hardcoding the list: a new metric shows up here on its own.

{
  "currency": "BRL",
  "timezone": "America/Sao_Paulo",
  "metrics": [
    {
      "id": "mrr",
      "group": "revenue",
      "unit": "money",
      "aggregation": "point_in_time",
      "higher_is_better": true
    },
    {
      "id": "mrr-churn",
      "group": "churn",
      "unit": "percent",
      "aggregation": "rate_over_period",
      "higher_is_better": false
    }
  ]
}
MetricUnitHow to read it
mrrMoney (cents)Snapshot at bucket end
arrMoney (cents)Snapshot at bucket end
subscribersCountSnapshot at bucket end
growthRate (fraction)Rate over the bucket
gross-revenueMoney (cents)Sum over the bucket
gateway-feesRate (fraction)Rate over the bucket
refund-rateRate (fraction)Rate over the bucket
net-revenueMoney (cents)Sum over the bucket
one-off-revenueMoney (cents)Sum over the bucket
mrr-churnRate (fraction)Rate over the bucket
net-mrr-churnRate (fraction)Rate over the bucket
customer-churnRate (fraction)Rate over the bucket
ltvMoney (cents)Snapshot at bucket end
arpaMoney (cents)Snapshot at bucket end
trialsCountSum over the bucket
trial-conversionRate (fraction)Rate over the bucket
freemiumsCountSum over the bucket
freemium-conversionRate (fraction)Rate over the bucket

Subscriptions

The heart of the source. This is where your MRR comes from -- with no subscriptions there is no metric to compute at all.

Create or update subscriptions

POST/v1/subscriptions

Creating and updating are the SAME call: the id is yours, and sending the same id again rewrites the object instead of duplicating it.

FieldTypeRequiredWhat it is
idstringThe identifier YOU choose. Sending the same id again updates the subscription, it doesn't duplicate.
customerstringThe id of the customer who owns this subscription. Leave it blank and the subscription counts as one customer (you don't have to register customers at all).
currencyenum(BRL | USD | EUR | GBP | ...)This subscription's currency, ISO-4217. An unknown currency is rejected, never assumed. It belongs to the whole subscription, at the top (never inside an amounts band), and does not change.
started_atdateWhen it started. The first band in amounts must start exactly here.
canceled_atdateWhen it was canceled. This is the churn date, and it is declared: there is no gateway here to tell us.
trial_enddateWhen the free trial ended. The period up to this date never becomes MRR.
amountsarray<{ from: date, cents: integer, cycle: enum(daily | weekly | biweekly | monthly | bimonthly | quarterly | semiannually | yearly), plan?: string }>The HISTORY of the amount, dated. Each band has from (since when that contract took effect), cents, cycle (how often you charge for it) and an optional plan (the product label for that period, which only has an effect if your source declares plans). The cycle lives inside the band, not at the top of the subscription: that's what lets you declare a monthly to yearly upgrade without canceling and recreating the subscription, which would invent a churn that never happened. The bands go in ascending date order, with no two on the same day, and the first one starts exactly at started_at. Read the section above before touching this.
curl -X POST https://api.metricaas.ai/v1/subscriptions \
  -H "Authorization: Bearer mtc_live_..." \
  -H "Content-Type: application/json" \
  -d '{"id":"sub_001","currency":"BRL","started_at":"2024-01-10","amounts":[{"from":"2024-01-10","cents":9900,"cycle":"monthly","plan":"Pro"},{"from":"2024-06-10","cents":99000,"cycle":"yearly","plan":"Pro anual"}]}'

The object you send REPLACES the one that was there: there is no merge. An omitted field is a DELETED field -- to change just the phone, send the whole customer, with the new phone. That is why this API has no PUT: POST already is the "replace entirely".

Send in BATCH (an array in the body, up to 1000 objects). Either all of it lands, or none of it does: a half-applied batch would leave you unsure what went in. The answer is 202, and your dashboard moves within a minute.

Canceling is this same POST, with canceled_at filled in: there is no separate endpoint, because there is no separate door. The subscription stays in history and becomes churn on the date you declare. Canceling is not deleting

Delete a subscription from history (the erratum)

DEL/v1/subscriptions/{id}

Use it when the data came in wrong: wrong id, a test in production, a duplicate. It is irreversible through the API -- to bring it back, send the object again.

DELETE is not canceling. Deleting says "this never existed": the object is gone and past MRR is rebuilt without it. To say the customer LEFT, send canceled_at -- the subscription stays in history and becomes churn on the declared date. Confusing the two corrupts your churn in both directions.

curl -X DELETE https://api.metricaas.ai/v1/subscriptions/sub_001 \
  -H "Authorization: Bearer mtc_live_..."

List subscriptions

GET/v1/subscriptions

Returns what is in your vault, paginated by cursor. That is your raw declaration; for the NUMBERS computed from it, see reading metrics at the top of this page.

curl "https://api.metricaas.ai/v1/subscriptions?limit=100&cursor=sub_001" \
  -H "Authorization: Bearer mtc_live_..."

Up to 500 per page (limit, defaults to 100). The cursor is the id of the last object on the previous page.

Customers

Optional. Only needed if one customer has more than one subscription -- with no customers, each subscription counts as one customer. The only required field is id, so you can group without telling us who they are.

Create or update customers

POST/v1/customers

Creating and updating are the SAME call: the id is yours, and sending the same id again rewrites the object instead of duplicating it.

FieldTypeRequiredWhat it is
idstringThe identifier YOU choose. Sending the same id again updates the customer, it doesn't duplicate.
namestringCustomer name. Optional: you never have to tell us who they are.
emailstringCustomer email.
phonestringThe customer phone number, as you write it. Used to find them in search and to reach out.
created_atdateWhen they became your customer.
curl -X POST https://api.metricaas.ai/v1/customers \
  -H "Authorization: Bearer mtc_live_..." \
  -H "Content-Type: application/json" \
  -d '{"id":"cus_001","name":"Empresa Exemplo","email":"contato@exemplo.com","phone":"+55 11 98888-7777"}'

The object you send REPLACES the one that was there: there is no merge. An omitted field is a DELETED field -- to change just the phone, send the whole customer, with the new phone. That is why this API has no PUT: POST already is the "replace entirely".

Send in BATCH (an array in the body, up to 1000 objects). Either all of it lands, or none of it does: a half-applied batch would leave you unsure what went in. The answer is 202, and your dashboard moves within a minute.

Delete a customer from history (the erratum)

DEL/v1/customers/{id}

Use it when the data came in wrong: wrong id, a test in production, a duplicate. It is irreversible through the API -- to bring it back, send the object again.

DELETE is not canceling. Deleting says "this never existed": the object is gone and past MRR is rebuilt without it. To say the customer LEFT, send canceled_at -- the subscription stays in history and becomes churn on the declared date. Confusing the two corrupts your churn in both directions.

curl -X DELETE https://api.metricaas.ai/v1/customers/cus_001 \
  -H "Authorization: Bearer mtc_live_..."

List customers

GET/v1/customers

Returns what is in your vault, paginated by cursor. That is your raw declaration; for the NUMBERS computed from it, see reading metrics at the top of this page.

curl "https://api.metricaas.ai/v1/customers?limit=100&cursor=cus_001" \
  -H "Authorization: Bearer mtc_live_..."

Up to 500 per page (limit, defaults to 100). The cursor is the id of the last object on the previous page.

Payments

Optional. Without payments, MRR stays whole: it comes from the contract, not from the money that arrived.

Create or update payments

POST/v1/payments

Creating and updating are the SAME call: the id is yours, and sending the same id again rewrites the object instead of duplicating it.

FieldTypeRequiredWhat it is
idstringThe identifier YOU choose.
customerstringThe id of whoever paid. Either this or subscription: a payment always says whose it is.
subscriptionstringThe id of the subscription this payment settles. Blank means a one-off sale (no MRR; it counts as revenue).
amount_centsintegerThe gross amount paid, in cents.
paid_atdateWhen the money came in.
fee_centsintegerThe payment processing fee, in cents.
refunded_centsintegerHow much went back to the customer, in cents. Partial refunds are valid.
currencyenumIf left blank, we use the subscription's (or your account's, for a one-off).
curl -X POST https://api.metricaas.ai/v1/payments \
  -H "Authorization: Bearer mtc_live_..." \
  -H "Content-Type: application/json" \
  -d '{"id":"pay_001","customer":"cus_001","subscription":"sub_001","amount_cents":9990,"paid_at":"2024-02-10","fee_cents":349}'

The object you send REPLACES the one that was there: there is no merge. An omitted field is a DELETED field -- to change just the phone, send the whole customer, with the new phone. That is why this API has no PUT: POST already is the "replace entirely".

Send in BATCH (an array in the body, up to 1000 objects). Either all of it lands, or none of it does: a half-applied batch would leave you unsure what went in. The answer is 202, and your dashboard moves within a minute.

Delete a payment from history (the erratum)

DEL/v1/payments/{id}

Use it when the data came in wrong: wrong id, a test in production, a duplicate. It is irreversible through the API -- to bring it back, send the object again.

DELETE is not canceling. Deleting says "this never existed": the object is gone and past MRR is rebuilt without it. To say the customer LEFT, send canceled_at -- the subscription stays in history and becomes churn on the declared date. Confusing the two corrupts your churn in both directions.

curl -X DELETE https://api.metricaas.ai/v1/payments/pay_001 \
  -H "Authorization: Bearer mtc_live_..."

List payments

GET/v1/payments

Returns what is in your vault, paginated by cursor. That is your raw declaration; for the NUMBERS computed from it, see reading metrics at the top of this page.

curl "https://api.metricaas.ai/v1/payments?limit=100&cursor=pay_001" \
  -H "Authorization: Bearer mtc_live_..."

Up to 500 per page (limit, defaults to 100). The cursor is the id of the last object on the previous page.