Nitx Publisher
Protocol reference
The wire contract every Nitx Publisher SDK implements: identifiers, endpoints, payloads, personalization and the shared mobile API.
1. Identifiers and existing pieces
- Website source code:
PUB-AM-XXXXXX(ad_websites.code).ad_websites.urlis the registered site. - Mobile source code:
SDK-AM-XXXXX(out of scope for the browser SDK). - Slot code:
SLOT-XXXXXX(ad_slots.code).ad_slots.format_typeis a free string, normallyWIDTHxHEIGHT. - Ad: an advertiser creative booked on one slot. Fields the SDK sees:
uuid,name,format(free string, normallyWIDTHxHEIGHT),path(public file URL or null),start,end(dates),status. Onlystatus = activeads inside their date window are "active". There is no click URL on an ad yet (section 6). - Backend JSON envelope: success
{ "success": true, "message": "...", "data": ... }, errors{ "success": false, "message": "..." }. 404s use that shape. - CORS: every origin is allowed with credentials; the SDK always uses
credentials: "omit"and plain GETs, so no preflight happens.
2. Publisher page markup and tag API
Unchanged from v1. Head tag printed by the dashboard:
Slot markup: <div class="nitx-ad" data-slot="SLOT-XXXXXX" data-size="300x250"></div>.
Commands: init(code, { api?, debug? }), refresh(), config({ api?, debug? }), enable('auto-ads') (ignored). Default API base https://api.nitx.app; non-production dashboards print { api: '<NEXT_PUBLIC_API_URL>' }.
data-nitx-state values: loading, rendered, empty, error. Rendering stays inside a sandboxed <iframe srcdoc> (sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox allow-top-navigation-by-user-activation"). Sizing precedence: data-size, then the slot's format_type from config, then the ad's format, then 100% x 250. none/no ad collapses the element unless data-size was given.
3. Backend endpoints the SDK calls (all live on dev)
All public, no auth, no throttle group. Plain GET, Accept: application/json.
3.1 GET /api/ad-websites/show/{code} (config)
Returns the website with its source and slots.
404 Ad website not found for an unknown code. The SDK parses width/height from each slot's format_type itself (^\d+x\d+$).
3.2 GET /api/ad-websites/init/{code} (activation)
Existing route, throttled 30/min per IP. Source lifecycle on dev is pending_verification (tag not seen yet) -> pending_approval (tag seen, Nitx admin still has to approve) -> active, or rejected. This route moves pending_verification to pending_approval and broadcasts ad.website.verified on channel ad-website.{code} so the dashboard's Echo listener fires; admin approval is a separate step. The SDK calls it once per page load, only when 3.1 reported ad_source.status === "pending_verification"; for every other status it never calls init. Responses are 200 either way (Ad source verified successfully or Ad source already active); 404 when unknown. Failures are ignored. Slot creatives (3.3) are served regardless of the source status; the backend does not gate on it today.
3.3 GET /api/ad-slots/show/{code} (creatives for one slot)
Returns the slot with its active ads (status = active, start <= today <= end).
404 Ad slot not found for an unknown code. One call per placement; the SDK issues them in parallel after config (or immediately if config failed).
Selection: the SDK ignores ads whose path is null. With several usable ads it picks one at random (filler_content = static picks the first by created_at ascending; the response order is used as given). The chosen ad renders as:
imagewhenpathends in png, jpg, jpeg, gif, webp, svg (case-insensitive, query string ignored);videowhen it ends in mp4, webm (muted, autoplay, loop, playsinline);- otherwise
image(best effort). No ad with a usablepathmeansempty.
Clicks: ads have no destination yet. The SDK renders the creative without a link. When the backend adds click_url to the ad resource (section 6), the SDK wraps the creative in <a href="{click_url}" target="_top" rel="noopener">; it must read that field if present and ignore its absence.
4. Tracking (not available on dev yet)
There is no impression, viewable or click tracking on dev. The SDK keeps its event machinery (IntersectionObserver viewability, batching) but sends nothing: with no impression_id from the backend there is nothing to report. When section 6 lands, the SDK registers an impression per rendered ad and resumes the v1 event and click behaviour. Until then:
window.__nitxPublisher.state()still countsrendered,empty,errors,requests;events.sentstays 0.- No request is ever made to
/api/sdk/publisher/*.
7. Personalization (SDK v1.2, collect-only until the backend ships section 8)
7.1 Publisher commands
init/config options added: personalization (default true; false means no storage, no visitor block, the three commands are no-ops), geolocation ('none' default or 'prompt'), ads ('auto' default, 'get' or 'post' as a developer override).
The SDK never reads the publisher's forms, cookies or DOM for personal data. Identity enters only through identify. The SDK shows no consent UI; the publisher's own banner calls consent.
7.2 Browser storage
Written only when personalization is on:
Normalisation before hashing: email trimmed and lowercased, must look like an address; phone reduced to E.164 (+ and 8 to 15 digits; numbers without a country code are dropped); gender mapped to m, f, other; birth_year 1900..current year (or derived from age); city and country trimmed, lowercased, max 64 chars (2-letter country kept as ISO alpha-2); traits max 20 keys of [a-z0-9_]{1,32} with string, number or boolean values (strings cut at 64). Hashes are SHA-256 hex via crypto.subtle; on insecure http origins hashing is unavailable, the field is omitted and state().personalization.hashing is false. Raw values are never stored or sent.
7.3 Mode flag in the website response (3.1)
The backend adds an optional block to GET /api/ad-websites/show/{code}:
Absent block or null keys mean "not available" and the SDK stays on the per-slot GETs of 3.3, sending nothing from this section on the wire. Paths must be relative to the API base; anything else is ignored. The backend can therefore roll personalization out per source. The SDK also falls back to GET for the rest of the page load when the POST answers 404, 405 or 501.
7.4 POST {ads_endpoint} (replaces 3.3 when the flag is present)
Content-Type: text/plain, body is JSON, credentials: omit, one call per batch of placements. This supersedes the page/viewport body in the v1 PDF the backend already has.
Rules: without consent visitor carries only id and consent: null; identity fields and geo appear only with consent; with personalization: false the block is omitted. page.url and page.referrer are stripped to origin and path (no query string); only the five utm_* keys are forwarded. device is mobile, tablet or desktop derived from viewport width and touch support. The SDK never sends IP, user agent or IP-derived location; the backend reads those from the request.
Validation the backend should apply: slots 1 to 50 unique codes belonging to the source; visitor.id UUID; hashes ^[0-9a-f]{64}$; page.url max 2048; unknown fields ignored.
Response data (the v1 shape, so events and clicks resume unchanged):
A slot missing from ads[] renders empty. click_url is the backend click redirect, never the advertiser URL.
7.5 Events and forget
POST {events_endpoint}, text/plain JSON { "events": [ { "impression_id": "<uuid>", "type": "impression" | "viewable", "at": "<ISO>" } ] }, 1 to 100 per request, flushed every 2 s and on page hide with keepalive. Responds 204. Sets rendered_at / viewable_at once.
GET /api/sdk/publisher/click/{impression_id} records the click once and 302s to the ad's destination.
POST {forget_endpoint}, text/plain JSON { "visitor": "<uuid>" }, 204. Deletes the profile and consent record for that id and unlinks it from stored impressions.
10. Mobile SDKs (Android, iOS, React Native, Flutter)
Four packages, one contract. Kotlin (Android) and Swift (iOS) are the implementations; React Native and Flutter are thin bridges over them and expose the same API. Everything in sections 7 and 8 (visitor id, consent, identify, hashing, context, POST ads/events/forget) applies unchanged; this section adds what is specific to apps.
10.1 Source, activation, config
- A mobile source has code
SDK-AM-XXXXX(ad_mobiles.code), aplatform(androidorios) and abundle_id. One source per app per platform. - Config:
GET /api/ad-mobiles/show/{code}returnsdata.code,data.platform,data.bundle_idanddata.ad_sourcewithstatusandslots[]exactly like 3.1. The SDK checks thatbundle_idequals the running app's package or bundle identifier and logs a warning on mismatch (never blocks in v1). - Activation:
GET /api/ad-mobiles/init/{code}once per process start, only whilead_source.status === "pending_verification"(moves it topending_approval, broadcastsad.mobile.verified). Same rule as 3.2. - Creatives today:
GET /api/ad-slots/show/{code}per slot (3.3). Personalized serving tomorrow: thesdkblock in the config response switches the SDK toPOST ads(7.3, 7.4). Mobile uses the same request body with the app context block of 10.4.
10.2 Ad formats and slot kinds
A slot's format_type names its kind. Web keeps using WIDTHxHEIGHT; mobile uses these values (the dashboard's slot form for mobile sources must offer exactly this list):
Both path-only ads (today's backend) and structured ads (10.3) work in every kind. For native, a path-only ad becomes { headline: ad.name, image: path } so apps render something sensible before the backend adds assets.
10.3 Ad payload for apps
The ad object in both 3.3 (ads[]) and 7.4 (data.ads[]) gains an optional assets block for native and an explicit creative type. Backend addition, additive and optional:
SDK mapping: creative when present, else by path extension (as the web SDK does). assets only for native; missing fields are null and the app must tolerate any of them being absent. click_url as in 7.4; when absent the creative is not clickable.
10.4 App context block
Replaces page in the 7.4 context object when the request comes from an app:
app.screen is the route or screen name the app passes when it loads an ad (optional). ad_id is the Google advertising id on Android, or the IDFA on iOS only after the App Tracking Transparency prompt was accepted; limited mirrors the platform's limit-ad-tracking flag; the block is null when unavailable or denied. The backend treats ad_id.value as a second matching key next to the hashed email and phone; never as a requirement.
10.5 Public API (identical across the four packages)
Rendering rules: image and video creatives use native image and video players (no WebView); html creatives use a WebView only in banner and interstitial kinds. Clicks open click_url in Chrome Custom Tabs or SFSafariViewController. Impression = creative rendered on screen; viewable = at least 50 percent visible for 1 second (banners and native), or shown (interstitial, app open). Full-screen formats show a close control after at most 5 seconds and always on video end.
10.6 Storage and privacy on device
SharedPreferences / UserDefaults under the nitx. prefix: visitor_id, consent, profile (hashes only), app_open_last_shown. Same lifetimes as 7.2. The ad id is read at start and after consent changes, never persisted beyond the process. Both SDKs ship the store declarations: an Apple privacy manifest (PrivacyInfo.xcprivacy) and the Play data safety entries, listing exactly: hashed contact info (optional, with consent), coarse location (from IP, server side), device id (optional), app interactions (ad impressions and clicks).
10.7 React Native and Flutter
Bridges only: they call the native SDKs through the platform channel and expose the same calls and a NitxBanner / NitxNativeAd component. No ad logic, hashing or storage lives in JavaScript or Dart. Versioning: the bridge pins a native version; context.sdk reports both, e.g. rn-1.0.0/android-1.0.0.