Choose a platform
Pick a platform + set your external_ref (your namespace for
addressing end-users — isolates your test data). Then bind.
Not a secret (IN-6 — your own addressing namespace). Auto-generated per browser session so concurrent testers don't bind/overwrite each other's accounts; edit it to reuse a specific one. Kept across the OAuth redirect round-trip.
Request sent to binding
Exactly what this app's backend sent to /api/v3/oauth/start
(key held server-side, never sent to this page) — and what it returned.
OAuth start
GET /api/v3/oauth/startLoading example…
Click Bind above to see a live run here.
Auth result
The token is exchanged + stored server-side by BDDAI — never returned here. The
redirect back only ever carries ?status=.
Loading example…
Complete a Bind above to see a live result here.
Callback contract — what binding actually redirected back to:
Access token: held server-side by BDDAI, never returned to this app —
the redirect back only ever carries ?status=.
Bound accounts
GET {service_base}/api/v3/public-access/{platform}/ad-accounts?external_ref= — scoped to this end-user.
Loading example…
Complete a Bind above to see a live fetch here.
Fetch data
Insights family only (meta/instagram/tiktok/google/google_analytics — shopee/shopify
are orders-family, not wired here) — GET /{platform}/data for one account,
no date range given (service defaults to the last 30 days).
SV-C6 landed — period (day/week/month/year) and breakdown/filter are now
server-side (breakdown_interval/group_by/
filter_by+filter), not client-side aggregation like before. Period
comparison (current vs previous, delta) is still fetch-two-ranges +
compute client-side — no built-in compare param.
ℹ️ filter matches on campaign name (regex substring,
case-insensitive) — not a numeric campaign ID. Single value: verified correct (narrows the
summary exactly as expected, and filter_exclude gives the exact complement).
Known issue (found live, not a demo bug): multiple values are currently
ANDed together server-side, not OR'd — filtering by two distinct campaign names returns
empty instead of their combined total. Use one value at a time for now.
Loading example…
Complete step 4 above to pick an account here.
Precedence when multiple are set (server-side, verified against the code): Breakdown > Period > Filter > none (plain summary).
Metric catalog
Own platform picker below — independent from Step 1's Choose Platform, no bind needed. Trace any platform's metric list/config directly. Owner decision: metric access is not gated per-metric — once a platform is granted, its whole catalog is open (no per-metric allowlist below).
⚠️ Catalog = full intended metric list. Actual availability on
/{platform}/data fully aligns once service's MV-1 lands
(TikTok is currently a subset) — source moves to MV-2 (data-driven)
later.
API Contract & Schema
Direct endpoints your backend calls. Base = per-environment host below + path. Every call is server-side with your key.
Base URLs (per environment)
| Placeholder | Service | Staging | Production |
|---|---|---|---|
| {binding_base} | Binding · OAuth broker (ingest) | https://binding.stg.bdd.ai | https://binding.bdd.ai |
| {service_base} | Service · Public Access (consume) | https://service.stg.bdd.ai | https://service.bdd.ai |
Legacy *.up.railway.app / *.devbdd.com = deprecated.
Reference harness (this demo): staging demo.broker.stg.bdd.ai ·
prod demo.broker.bdd.ai. Your app integrates directly with
{binding_base}/{service_base} — this demo is a reference
harness only, not an integration target.
Authentication
Authorization: Bearer pk_live_… # purpose='ingest' · server-side only
Guard order: missing/invalid scheme → 401 · unknown/inactive →
401 · expired → 401 · IP not allowed →
403 · platform ∉ allowed_platforms →
403 · source suspended → 403.
Endpoints
6 direct| # | Method · Path | Key params | Response | Errors |
|---|---|---|---|---|
| A | POST {service_base}/api/v3/public-access/users | external_ref | 200 {user_id} | 401·403·422 |
| B | GET {binding_base}/api/v3/oauth/start | platform·source_app·return_url·external_ref | 302 → consent | 400·401·403·404·503 |
| C1 | GET {service_base}/…/{platform}/ad-accounts | external_ref | 200 {data:[…]} | 401·403·404·422·429 |
| C2 | GET {service_base}/…/{platform}/data | account_id·external_ref·date_start·date_end·breakdown_interval·group_by·filter_by·filter | 200 {data,pagination} | 401·403·404·422·429 |
| C3 | GET {service_base}/…/{platform}/metrics | (platform-gated, no ref) | 200 {data:[…]} | 401·403 |
| C4 | GET {service_base}/…/{platform}/orders/{overview|periodic|distribution} | account_id·external_ref·breakdown_interval | 200 (gated 503) | 401·403·404·422 |
Response schema — /data (insights)
{ "data": { "spend": 48200000, "impressions": 3400000, "clicks": 92100 }, "pagination": null }
# no dimension flags -> 1 aggregate dict. KPI cards.{ "data": [ { "date": "2026-07-01", "spend": 1600000 }, … ], "pagination": { "page":1, "page_size":100, "total":30, "total_pages":1 } }
# show_date=true OR breakdown_interval=day|week|month|year -> row per-period.{ "data": [ { "campaign": "Sneakers", "spend": 12000000 }, … ],
"pagination": { "page":1, "page_size":50, "total":7, "total_pages":1 } }
# group_by/campaign/objective -> row per-dimension (paginated). Ad-level device/placement = facebook-only.
Required: account_id+external_ref. Date defaults to the last
30 days; max span 92 days; page_size ≤ 1000. Data is
warehoused, not a live-API passthrough.
Errors & Platform Vocabulary
HTTP status meanings + the bind↔consume platform-string mapping (the guard maps them automatically).
Error reference
| Status | Meaning |
|---|---|
| 400 | Raw user_id on external path, or unknown platform value. |
| 401 | Missing / invalid / expired key (or JWT on internal path). |
| 403 | purpose≠ingest · platform ∉ allowed_platforms · return_url not allowlisted · source suspended · cross-source ref · account not owned. |
| 404 | source_app not found · external_ref unknown for this source · unknown platform/report. |
| 422 | external_ref empty/too long · key not scoped · return_url not absolute · invalid date range / page_size. |
| 429 | Rate-limited (respect Retry-After). |
| 503 | Platform wired but gated (e.g. Shopee). |
Platform vocabulary & availability
| bind platform | grant allowed_platforms | consume path | Status |
|---|---|---|---|
| google_ads | /gadwords/… | LIVE | |
| google_analytics | google_analytics | /ganalytics/… | LIVE |
| meta | /facebook/… | LIVE | |
| /instagram/… | LIVE | ||
| tiktok | tiktok | /tiktok/… | LIVE |
| shopee | shopee | /shopee/orders | GATED 503 |
| shopify | shopify | /shopify/orders | DISABLED |