Reporting API
The Reporting API lets you render Crossdeck's data — revenue, errors, read-cost, per-host analytics, and the cross-layer cross-match — inside your own product. You call it from your backend with a secret key; the data lands as clean JSON aggregates. It is the only API that can answer a question across layers — "why did this error happen, who did it affect, and how much do they pay you?" — because Crossdeck owns the identity that joins those layers. That join is /v1/crossmatch.
Authentication
Every Reporting endpoint requires a secret key on https://api.cross-deck.com:
Authorization: Bearer cd_sk_live_…
- Publishable keys (
cd_pub_…) are rejected (401 secret_key_required). These endpoints read private data; a publishable key lives in browsers and can't be trusted to scope it. Never call the Reporting API from a browser — only from your server. - The key resolves to your project. You never pass a project id; scope is derived from the key. There is no path by which one project's key can read another project's data.
- Derive any
host/customerIdfrom your own authenticated session, never from client input. That's what keeps one of your own users from reading another's data.
Conventions
- Aggregates, not rows. Endpoints return counts and totals — never a named customer, an individual charge, or a stack trace. The per-customer
/v1/crossmatchis the one identity-scoped surface, and it returns only counts and amounts for the one customer you ask about. - Environments are isolated. A key is scoped to
productionorsandbox. Passing a mismatched?envis a hard403. - Every response carries
meta.requestId(echo it in support tickets) andmeta.generatedAt.
Errors
{ "error": { "type": "authentication_error", "code": "secret_key_required",
"message": "…", "request_id": "req_…" } }
| HTTP | type | When |
|---|---|---|
400 | invalid_request_error | missing or invalid parameter |
401 | authentication_error | missing / non-secret / invalid key |
403 | permission_error | host not owned by your project, or ?env mismatch |
429 | rate_limit_error | over the rate limit (see Retry-After) |
Rate limits
~2 requests/second sustained, 120 burst, per key. Over the limit returns 429 with a Retry-After header. Generous because every call is an O(range) point-read — capped to deter scraping.
GET /v1/revenue
Project-wide revenue, from the maintained MRR ledger.
| Param | ||
|---|---|---|
granularity | optional | total (default) or day |
days | optional | with granularity=day, window length (≤366, default 90) |
GET /v1/revenue?granularity=day&days=30
Authorization: Bearer cd_sk_live_…
200
{
"data": {
"currency": "usd",
"current": { "mrrCents": 412900, "payingCustomers": 318,
"byRail": { "stripe": 280100, "apple": 110200, "google": 22600 } },
"series": [ { "date": "2026-06-01", "mrrCents": 401200, "payingCustomers": 309, "byRail": {…} } ]
},
"meta": { "generatedAt": "…", "freshness": "ledger", "requestId": "req_…" }
}
GET /v1/errors
One issue, stitched to the identities it touched — the moat, in the errors layer.
| Param | ||
|---|---|---|
fingerprint | required | the issue id |
GET /v1/errors?fingerprint=a1b2c3…
200
{
"data": {
"fingerprint": "a1b2c3…", "exceptionType": "TypeError", "level": "error",
"status": "open", "occurrences": 142,
"firstSeen": "…", "lastSeen": "…", "lastRelease": "1.4.2",
"affected": { "users": 40, "payingUsers": 12 }
},
"meta": { … }
}
affected.users = how many distinct people hit it. affected.payingUsers = how many of them pay you. Never a stack trace, never the exception message (it can carry user data), never a named user.
GET /v1/buckets
Read-cost: the per-user-vs-overhead split + reads by operation.
| Param | ||
|---|---|---|
days | optional | window (≤90, default 30) |
200
{
"data": {
"window": { "days": 30, "from": "2026-05-26", "to": "2026-06-25" },
"totalReads": 1840221,
"split": { "perUserReads": 1410880, "overheadReads": 429341, "overheadPct": 23 },
"identifiedUsers": 614,
"byOperation": [ { "operation": "analytics", "reads": 980400 }, … ]
},
"meta": { … }
}
perUserReads + overheadReads = totalReads, by construction. Overhead is your account's own background/platform reads — the un-attributed remainder.
GET /v1/crossmatch
The moat in one call. One customer, every layer Crossdeck owns, stitched by identity. Identify the customer with any of: customerId (a cdcust_…), userId (your own id), anonymousId, appleOriginalTransactionId, googlePurchaseToken, stripeCustomerId.
GET /v1/crossmatch?userId=agent_8842
200
{
"data": {
"customer": { "crossdeckCustomerId": "cdcust_…" },
"revenue": { "monthlyCents": 500, "paying": true },
"entitlements": { "active": 2 },
"readCost": { "reads": 4021, "windowDays": 30 }
},
"meta": { … }
}
The sentence no competitor can produce: this customer pays $5/mo, holds 2 entitlements, and cost you 4,021 reads. data is null if the identifier resolves to no customer. Entitlement detail lives in GET /v1/entitlements.
GET /v1/reporting/metrics
Per-host analytics — for apps with many subdomains, each user sees only their own (wes.example.com).
| Param | ||
|---|---|---|
host | required | must belong to your project (validated against your verified origins) |
granularity | optional | total (default) or day |
days | optional | window (≤90, default 30) |
200
{
"data": {
"host": "wes.example.com",
"range": { "from": "…", "to": "…", "granularity": "day" },
"totals": { "views": 4210, "uniqueVisitors": 1890 },
"series": [ { "date": "2026-06-01", "views": 210, "uniqueVisitors": 95 } ]
},
"meta": { "freshness": "rollup", … }
}
A host your project doesn't own returns 403 — never a confirm/deny of another tenant's host.
GET /v1/reporting/breakdown
| Param | ||
|---|---|---|
host | required | as above |
dimension | optional | top_pages (default) or top_referrers |
days | optional | window (≤90, default 30) |
limit | optional | rows (≤100, default 25) |
200
{
"data": { "host": "wes.example.com", "dimension": "top_pages",
"rows": [ { "key": "/calculator", "count": 1820 }, { "key": "/apply", "count": 410 } ] },
"meta": { "truncated": false, … }
}
The cross-match is the point
Don't think of these as six separate readouts. Identity joins them. The same person is a row in your revenue, a name behind an error, a slice of your read-cost, and a visitor in your analytics — and Crossdeck is the only thing that knows it's the same person. Start from any layer and pivot to the others:
- From an error →
/v1/errorsgives you who and how many pay. - From a customer →
/v1/crossmatchgives you revenue × entitlements × read-cost.
Related
- API reference — the full REST surface: events, entitlements, identity, purchases.
- API keys — issuing and rotating the
cd_sk_secret keys these endpoints require. - Entitlements — what
crossmatch.entitlementsand/v1/entitlementsresolve.