Skip to content

API · Statistics — pull your numbers into the tools you already use

The statistics endpoints return the same numbers you see in the Statistics section — leads, conversions and per-publication performance — so you can take them to Looker Studio, Google Sheets, n8n or your own backend.

This page covers only those endpoints. How to get the key, how to authenticate, the response shape, the rate limit and the errors shared by the whole API live in ZeroChats API. Start there if this is your first call.

The base URL is:

https://app.zerochats.com/api/statistics/v1

Every call is a GET and the key travels in the Authorization header:

Ventana de terminal
curl -H "Authorization: Bearer zcst_your_key" \
https://app.zerochats.com/api/statistics/v1/overview

On these endpoints meta always comes back, and it tells you which period and which time zone produced the numbers:

{
"success": true,
"data": {},
"meta": {
"startDay": "2026-07-01",
"endDay": "2026-07-30",
"timezone": "Europe/Madrid"
}
}
EndpointTakes datesWhat it returns
/meNoThe business the key belongs to, and its time zone.
/overviewYesLifetime totals and the funnel by stage, split into inbound and outbound.
/leads/dailyYesOne row per day: leads received and how far they got.
/leads/by-weekdayNoLifetime distribution of inbound leads across the days of the week.
/leads/by-hourNoLifetime distribution of inbound leads across the hours of the day.
/conversions/goalsYesPer sales link and send day: in Proposal and converted.
/conversions/tagsYesLeads converted (Booked or Paid) per tag and day.
/conversions/contentsYesPer content piece and send day: in Proposal and converted.
/performanceYesPerformance per publication, ad or origin, with a link back to the post.
/welcome-messagesYesWelcome messages sent, answered and the response rate, per day.

The three endpoints that don’t take dates (/me, /leads/by-weekday and /leads/by-hour) cover the whole history of the business. Sending them startDay and endDay is not an error: they simply ignore them, and you can tell because meta comes back without dates.

{
"lifetime": { "totalLeads": 1840, "positiveLeads": 213 },
"funnel": {
"inbound": [
{ "state": "OPEN_CONVERSATION", "count": 48 },
{ "state": "LEAD", "count": 21 },
{ "state": "CLIENT", "count": 7 }
],
"outbound": [{ "state": "IN_PROGRESS", "count": 12 }]
}
}

lifetime is historical and does not depend on the range you ask for: totalLeads is every lead you have and positiveLeads the ones sitting in Proposal, Booked or Paid (the Proposal made figure, not conversions).

funnel does cover the range, and it separates inbound leads (they wrote to you) from outbound ones (you started the conversation). They travel apart on purpose: they are two different acquisition motions, and blending them hides which one converts.

Each stage arrives under its English name, exactly like in the Custom Webhook and the leads endpoints, and for the same reason: the names are identical across every account whatever language you work in, so a filter built on them doesn’t break.

Stage in the appstate
OpenOPEN_CONVERSATION
DiscoveringIN_PROGRESS
QualifyingQUALIFYING
ProposalLEAD
BookedBOOKED
PaidCLIENT
PostponedPOSTPONED
UnqualifiedNO_QUALIFY
LostLOST
[{ "date": "2026-07-01", "total": 34, "qualified": 6, "booked": 2, "paid": 1 }]
FieldWhat it is
dateThe day, as YYYY-MM-DD.
totalThat day’s conversations: leads your AI talked to, on the day of their last state change.
qualifiedOf those, the ones now sitting in Proposal.
bookedOf those, the ones now sitting in Booked.
paidOf those, the ones now sitting in Paid.

Days with no activity come back too, filled with zeros, so you can plot the series as-is without patching gaps.

[{ "dayOfWeek": 0, "day": "MONDAY", "total": 240, "qualified": 31, "booked": 14 }]
[{ "hour": 9, "total": 88, "qualified": 12, "booked": 5 }]

dayOfWeek runs from 0 (Monday) to 6 (Sunday) and day gives you the name so you never have to remember the convention. hour runs from 0 to 23. Both always come back complete: all seven days and all twenty-four hours, even the empty ones.

Like the day-of-week and hour-of-day charts in Statistics, they only count leads who wrote to you (inbound) and talked to your AI, at the moment they wrote. Here qualified means leads in Proposal and booked groups Booked and Paid together. Don’t try to reconcile these two endpoints row by row against /leads/daily: they answer a different question — what day and time people write to you — not how a cohort converted.

/conversions/goals, /conversions/tags and /conversions/contents

Section titled “/conversions/goals, /conversions/tags and /conversions/contents”

All three return the same shape, one row per entity and day:

[
{
"date": "2026-07-14",
"id": "9f1c8a4e-2b77-4d0a-9d1c-3f2b6a5c7e10",
"name": "Assessment call",
"leads": 4,
"clients": 1
}
]

name comes already resolved — the name of the sales link, the tag or the content piece — so you don’t need a second call or your own lookup table.

A conversion is a lead that is in Booked or Paid today; Proposal does not count.

  • In /conversions/goals and /conversions/contents, each row is a send day: leads are the people who received the link or content that day and are in Proposal today, and clients the ones in Booked or Paid today — the conversions.
  • In /conversions/tags, leads are the leads with that tag in Booked or Paid today, and clients equals leads: a tag is either assigned or it isn’t, so there is no lead/client split to report. Until 15 October 2026 this endpoint also counted leads in Proposal.

One row per publication, ad or lead origin in the period:

[
{
"kind": "media",
"id": "17900000000000000",
"name": null,
"mediaId": "17900000000000000",
"mediaType": "REELS",
"url": "https://www.instagram.com/reel/CxYzAbCdEfG/",
"leads": 120,
"answered": 86,
"qualified": 24,
"booked": 9,
"paid": 4,
"rates": { "answered": 71.67, "qualified": 20, "booked": 7.5, "paid": 3.33 }
}
]
FieldWhat it is
kindmedia (a publication or an ad), personalized (an origin you defined), inbound, outbound.
idIdentifier of the row. On media rows it is the Instagram publication’s.
nameThe name of the personalized origin. Empty on every other row.
mediaIdPublication identifier. null when the row is not a publication.
mediaTypeSTORY, FEED, REELS or AD. null when the row is not a publication.
urlPublic link to the post on Instagram. null when we don’t have it.
leadsLeads that engaged with that publication or origin.
answeredOf those, the ones who sent you a DM after a message of yours. Comments don’t count.
qualifiedOf those, the ones that reached Proposal, Booked or Paid.
bookedThe ones now in Booked.
paidThe ones now in Paid.
ratesThose same four numbers as a percentage of leads, to two decimals.

url is what turns a report into something actionable: every row of your dashboard can link to the real post, so whoever reads it goes from the number to the content in one click. It’s a normal Instagram link — stable, public and it doesn’t expire.

qualified includes those who booked and those who paid, so it reads as a funnel: qualified is wider than booked and paid, which exclude each other (a lead in Paid doesn’t count in booked). answered uses the same “Replied” rule as the app —a DM from the lead after a message of yours, whether or not it later ended up Lost—, explained in Statistics and reports. The outbound row is dated by the day the welcome was sent; the rest by the last state change.

[{ "date": "2026-07-01", "sent": 40, "responded": 11, "responseRate": 27.5 }]

responseRate is the percentage of sent to two decimals, and it is 0 on days when you sent nothing. Each welcome counts on the day it was sent, and responded follows the definition of Replied: a day’s sends never change, but its replies can still go up.

Endpoints marked “Takes dates” accept two parameters:

ParameterFormatWhat it is
startDayYYYY-MM-DDFirst day, included.
endDayYYYY-MM-DDLast day, included.
Ventana de terminal
curl -H "Authorization: Bearer zcst_your_key" \
"https://app.zerochats.com/api/statistics/v1/leads/daily?startDay=2026-07-01&endDay=2026-07-31"

There are four rules:

  • Both days are calendar days in your business’s time zone, not timestamps. If your business is in Madrid, July 1st runs from 00:00 to 23:59 Madrid time even if you fire the query from Mexico. That’s why meta.timezone travels in every response.
  • Send both or send neither. Sending just one is an error rather than a guess at the other.
  • If you send neither, you get the last 30 days, today included.
  • The maximum is 366 days. A longer range is an error; split the query into chunks.

An example of the first two rules together: asking for startDay=2026-07-01&endDay=2026-07-31 gives you the whole of July, all 31 days, first and last row included. Asking for startDay=2026-07-31&endDay=2026-07-01 is an error, because the start has to come before the end. And asking for only startDay=2026-07-01 is an error too, with the message startDay and endDay must be provided together.

All of those errors arrive as a 400 with error.code set to invalid_request: bad date format, only one of the two days, reversed order, or a range longer than 366 days.

If you need many days, ask for one wide range in a single call rather than a day per request: the daily endpoints already return the full series, and one 30-day call is faster and cheaper than thirty one-day calls.

A complete, real case: every Monday you want to know which publications brought you clients last week, with a link to each post so you can review them.

  1. Work out the range. Last Monday to last Sunday, as YYYY-MM-DD. For example startDay=2026-08-17 and endDay=2026-08-23.

  2. Call /performance with that range.

    Ventana de terminal
    curl -H "Authorization: Bearer zcst_your_key" \
    "https://app.zerochats.com/api/statistics/v1/performance?startDay=2026-08-17&endDay=2026-08-23"
  3. Keep the kind: "media" rows. Those are the publications and ads; inbound and outbound are the catch-all buckets and won’t help in this report.

  4. Sort by paid — or by rates.paid if you care more about efficiency than volume — and keep the top five.

  5. Build the message using url. Every line carries the link to the post, so whoever reads it can open it without hunting for it.

    Top content, 17–23 August
    1. REELS — 120 leads · 4 clients (3.33%) → https://www.instagram.com/reel/CxYzAbCdEfG/
    2. FEED — 86 leads · 3 clients (3.49%) → https://www.instagram.com/p/CxAbCdEfGhI/
  6. Publish it wherever you’ll actually read it: a Slack channel, an email, or a new row in a spreadsheet.

If you want business context in that same report, also call /leads/daily with the same range for the week’s total volume, and /conversions/goals to see which sales link closed those clients.

  • Store the time zone from meta alongside the data if you keep it. A number with no record of which calendar it was measured in can’t be compared to another one later.
  • Use id as your key, not name. Tag, content and sales-link names can be changed from the app; identifiers can’t.
  • Ask for ranges, not single days. One 30-day call is faster and cheaper than thirty one-day calls.

Problems with the key — a 401 on a freshly copied key, a key that stops working — are covered in ZeroChats API. These are the ones specific to statistics.

The numbers don’t match what I see in Statistics

Section titled “The numbers don’t match what I see in Statistics”

It’s almost always the range. Check you’re asking for the same days — remember both ends are included — and look at meta.timezone: if your tool works out dates in its own time zone and your business is in another, you’re comparing slightly different periods.

/leads/by-weekday and /leads/by-hour also apply a deliberately different criterion, explained above: booked there includes the leads that have already paid.

/performance returns more leads than I have

Section titled “/performance returns more leads than I have”

That’s expected and explained above: a lead who engaged several publications counts in all of them. For the business total, use /overview or /leads/daily.

Some /performance rows have url set to null

Section titled “Some /performance rows have url set to null”

It means we don’t have the public link for that publication — usually old or deleted content. Show the row anyway, using mediaType as its label, just without a link.