BDDAI External Integration Guide

BDDAI — External Integration Guide & API Contract

Binding accounts via the OAuth broker (ingest) and reading data via Public Access (consume), addressed per end-user by external_ref. Version 1.0 · 2026-07-15 · Status: as-built (IN-6 external_ref, shipped). A formatted .docx of this guide is available alongside this file.

1. Overview

BDDAI lets an external application (a source_app) connect its end-users’ ad / e-commerce accounts and read their data — without ever handling OAuth tokens. Your app holds one public-access key; you may serve many end-users under it.

2. Prerequisites

Ask a BDDAI administrator to provision (once):

  1. Register your app as a source_app — you receive a code (e.g. partnerx).
  2. Add your return URL to that source_app’s redirect allowlist — exact prefix match, default-deny: an unregistered return_url makes /oauth/start return 403 for every request.
  3. Issue a credential (purpose='ingest') scoped to the platforms you may bind (allowed_platforms). The plaintext key is shown once, at issue time.

Keep the key server-side — in an environment variable. Never put it in a frontend bundle, a log line, or any browser-visible response.

3. Authentication

Every request authenticates with the same key:

Authorization: Bearer pk_live_…

The key is hashed (HMAC-SHA256) and looked up server-side. Guard order: missing/invalid scheme → 401; unknown/inactive key → 401; expired → 401; IP not allowed (if the key is IP-locked) → 403; platform not in allowed_platforms403; source_app suspended → 403.

1-key model: a single ingest key is used for both binding and consuming in this reference setup. Whether real integrators get one key or two (least-privilege) is an administrator decision — confirm with your BDDAI contact.

4. Core concept — addressing end-users by external_ref

Your app never sends a raw BDDAI user id. Instead you send external_ref — a stable identifier in your own namespace (e.g. your user’s primary key or email). BDDAI resolves (source_app, external_ref) → an internal user id and holds the mapping.

You do not need to store any id BDDAI returns — keep using your own external_ref on every call.

5. Integration flow

Three phases: (A) provision the end-user, (B) bind a platform via the broker, (C) consume the account list and data. The token is exchanged and stored server-side; only ?status= is ever carried back to your app.

Integration flow diagram

5.1 Re-binding & refreshing access

An end-user who already bound can refresh or re-authorize without any special endpoint:

Reuse the same external_ref, no re-provisioning, no separate “rebind” endpoint. First-time and repeat binds are the exact same call.

6. Endpoint reference

A. Provision end-user

POST {service_base}/api/v3/public-access/users
Authorization: Bearer <key>        # purpose='ingest'; source_app derived from key
Content-Type: application/json
{ "external_ref": "<your-user-ref>" }   # required, 1..200 chars
→ 200 { "user_id": <int> }              # idempotent

Errors: 401 invalid key · 403 purpose≠ingest · 422 external_ref empty/too long, or key not scoped to a source_app. (Binding also auto-provisions on first bind, so an explicit call here is convenient but not strictly required.)

B. Start binding (OAuth broker)

GET {binding_base}/api/v3/oauth/start?platform=&source_app=&return_url=&external_ref=
Authorization: Bearer <key>        # a browser redirect cannot attach this header → call from your backend
→ 302  Location: <platform consent URL>   # do NOT follow server-side; redirect the browser to it

Errors: 401 auth · 400 unknown platform · 403 purpose/allowed_platforms/return_url-not-allowlisted/source suspended/cross-source ref · 404 source_app not found · 503 platform gated (e.g. Shopee).

Return (callback to your app)

{return_url}?status=success        # or ?status=failed

The token is never in the redirect or any client-visible response. status=failed = replayed/expired session OR downstream token-exchange failure; treat both the same (retry from step B).

C1. List bound accounts

GET {service_base}/api/v3/public-access/{platform}/ad-accounts?external_ref=
Authorization: Bearer <key>
→ 200 { "data": [ { "account_id", "account_name", "description" } ] }

Errors: 401 · 403 platform not allowed · 404 ref unknown · 422 ref empty / key unscoped · 429 rate-limited.

C2. Fetch insights data (meta / instagram / tiktok / google-ads / GA4)

GET {service_base}/api/v3/public-access/{platform}/data
  ?account_id=&external_ref=&date_start=&date_end=&campaign=&objective=&show_date=&page=&page_size=
→ 200 { "data": …, "pagination": { page, page_size, total, total_pages } | null }

Consume path per platform: facebook · instagram · tiktok (insights-family) · gadwords (Google Ads) · ganalytics (GA4). SV-C6 udah landing — periode (day/week/month/year) + table/breakdown + filter tersedia; model beda per grup:

Insights-family (facebook / instagram / tiktok): - Summary — tanpa dimensi: data = 1 dict agregat, pagination: null. → KPI cards. - Seriesshow_date=true (harian) ATAU breakdown_interval=day|week|month|year (SV-C6): row per-periode. → line/bar over time. - Table / breakdowncampaign=true/objective=true ATAU group_by=objective: row per-dimensi (paginated). Ad-level group_by=device|platform|placement|campaign = facebook SAJA (ig/tiktok → 422 data-gap, warehouse ga simpan field ad-level itu). - Filterfilter_by=campaign + filter=<id,..> (+ filter_exclude=true) → filter/exclude sampai level campaign.

Google Ads (gadwords): channel_type (search|display|shopping|video|…, v24, default search) + SV-C6: breakdown_interval=day|week|month (year → 422, follow-up) + group_by=<9 dimensi GAds>. → periodic + table (Slice A; bukan summary-only lagi).

GA4 (ganalytics): report=overview|acquisition_channel|acquisition_source_medium|geo_country|geo_city|page_screen|device (default overview) + SV-C6: breakdown_interval=day|week|month|year → time-series metric (Slice C). overview→summary; report berdimensi→list paginated.

account_id + external_ref wajib; date default 30 hari terakhir, max span 92 hari; page_size ≤ 1000. Errors: 401 · 403 platform/account bukan milik end-user · 404 ref unknown · 422 range/param invalid · 429.

Granularity & comparison (penting buat charting): - Insights /data granularity = harian (show_date). Butuh mingguan/bulananagregasi di FE-mu (server insights hanya harian). (Orders /orders/periodic beda: breakdown_interval=day|week|month native.) - Perbandingan antar-periode (current vs previous, delta %): fetch 2 range terpisah lalu hitung sendiri — tidak ada param compare built-in.

C3. Discover metric catalog (MV-2)

GET {service_base}/api/v3/public-access/{platform}/metrics   (Bearer key; platform-gated; TANPA external_ref)
→ 200 { "data": [ { code, title, format, decimal, calculate, metric_a, metric_b, operator, type, adv } … ] }

Katalog metric + config per platform (buat tau metric apa aja + cara format/rumus sebelum fetch /data). Errors: 401 · 403 platform belum di-grant ke key ini.

C4. Fetch orders report (shopee / shopify — currently gated)

GET {service_base}/api/v3/public-access/{platform}/orders/{overview|periodic|distribution}
  ?account_id=&external_ref=&date_start=&date_end=&breakdown_interval=day|week|month

overview→summary dict · periodic→series (breakdown_interval day/week/month) · distribution→breakdown table. Errors: 401 · 403 account not owned · 404 ref unknown / unknown report · 422 ref empty/unscoped.

7. Platform vocabulary & availability

The bind platform string differs from the consume path (account_platform vocab). Grant your key with the allowed_platforms value; the guard maps consume-vocab↔︎grant-vocab automatically (gadwords↔︎google_ads, ganalytics↔︎google_analytics).

platform (bind /oauth/start) allowed_platforms (grant) consume path / family Status
google google_ads /gadwords/… (insights · channel_type + periode/table SV-C6) Live — bind + consume (RL-4/SV-C6)
google_analytics google_analytics /ganalytics/… (insights, report=) Live — bind + consume (RL-3)
meta facebook /facebook/… (insights) Live — bind + consume
instagram instagram /instagram/… (insights) Live — bind + consume
tiktok tiktok /tiktok/… (insights) Live — bind + consume
shopee shopee /shopee/orders (orders) Gated (503)
shopify shopify /shopify/orders (orders) Disabled

8. Error reference

Status Meaning
400 Raw user_id sent on external path, or unknown platform value.
401 Missing / invalid / expired key (or JWT on internal path).
403 purpose≠ingest, platform not in allowed_platforms, return_url not allowlisted, source_app suspended, cross-source ref, or account not owned by this end-user.
404 source_app not found, external_ref unknown for this source, or unknown platform/report.
422 external_ref empty/too long, key not scoped to a source_app, return_url not absolute http(s), or invalid date range / page_size.
429 Rate-limited (respect Retry-After).
503 Platform is wired but gated (e.g. Shopee).

9. Security rules (must-follow)

10. Notes & current limitations