← BillDogCONTENT HISTORY
WHAT CHANGED
Update to BillDog
Snapshot Nov 15, 2026 · 12:08 UTC · version 1.1.0
Collection source: downloaded plugin package.
First saved snapshot
No earlier snapshot is available to establish a change.
Save BillDog and check future changes in Following.
Up to 3 plugins free · no card required.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"description": "Build a custom (bring-your-own-UI, headless) paywall on BillDog - fetch the offering a placement selects for this user, draw it with the app's own UI (SwiftUI, Jetpack Compose, React Native, Expo, Flutter, web, or a game engine over REST), buy a package, and handle holdout, withheld, gated and unknown placements and impressions correctly. Use when the user wants their own paywall screen instead of a BillDog-designed one, mentions headless or BYO-UI paywalls, or integrates BillDog into Unity, Unreal, Godot, a TV or desktop app without an SDK.",
"included_files": [],
"name": "billdog-headless",
"skill_md_contents": "---\nname: billdog-headless\ndescription: Build a custom (bring-your-own-UI, headless) paywall on BillDog - fetch the offering a placement selects for this user, draw it with the app's own UI (SwiftUI, Jetpack Compose, React Native, Expo, Flutter, web, or a game engine over REST), buy a package, and handle holdout, withheld, gated and unknown placements and impressions correctly. Use when the user wants their own paywall screen instead of a BillDog-designed one, mentions headless or BYO-UI paywalls, or integrates BillDog into Unity, Unreal, Godot, a TV or desktop app without an SDK.\n---\n\n# Headless (bring-your-own-UI) paywalls\n\nIn headless mode the server decides **what** to sell to this user (the offering,\nfrom the placement, its campaigns and experiments) and the app decides **how** to\nshow it. The app never receives a designed paywall: `paywall` is always null.\n\nThe full guide with complete samples for every platform is\nhttps://billdog.io/docs/headless-paywalls (Markdown:\nhttps://billdog.io/docs/headless-paywalls.md). Use it for anything this file does\nnot cover; do not guess an SDK method.\n\n## The rules, on every platform\n\n1. **Use the placement identifier the project has.** Read it with `list_placements`\n over the BillDog MCP server, or ask. An unknown placement is 404\n `PLACEMENT_NOT_FOUND`, surfaced as a typed error. It never falls back to another\n offering, so show nothing and report the typo.\n2. **`offering` is the selection, and it can be null.** Null means nothing was\n selected for this user: show nothing. Do not take an entry from `offerings`\n instead, and do not fall back to the current offering.\n3. **Read `presentation` before drawing.** All of these arrive as a normal success:\n - `holdout: true`: this user is in a holdout. Show nothing.\n - `withheld: true` (sent only when true), with `withheld_reason` `holdout` or\n `audience_excluded`: `offering` is null on purpose.\n - `gated: true` with `required_entitlement`: reaching this screen needs access,\n and in headless mode the app owns that gate. Check the entitlement and let a\n user who has it through.\n4. **Track only what you showed.** Call `trackPaywallPresented(offering, placement)`\n when the screen appears and `trackPaywallDismissed(offering, placement)` with the\n same offering when it goes away. Never track for a holdout, a withheld result or\n a null offering: a holdout is measured by the absence of an impression. No\n `paywall_id` is sent.\n5. **Fetch when you are about to show it.** A read that resolves a running\n experiment or campaign records the exposure and counts the campaign impression\n (and its frequency cap) whether or not the screen appears. Don't prefetch screens\n the user may never reach.\n6. **Unlock from entitlements, not from the purchase call alone.** After a purchased\n or restored result, read customer info and check the entitlement the screen\n sells.\n7. **Prices come from the store product on the package**, not from the offering's\n metadata. Never write a price into copy.\n\n## Platform API\n\n| Platform | Fetch | Result | Buy / restore | Unknown placement |\n|---|---|---|---|---|\n| iOS | `try await BillDog.shared.getOffering(placement:)` | `PlacementOffering`: `offering`, `presentation`, `shouldPresent` | `BillDog.shared.purchase(package:)` → `PurchaseResult`; `BillDog.shared.restore()` | throws `BillDogError.placementNotFound` |\n| Android | `BillDog.getOffering(placement)` (suspend; `BillDog` is a Kotlin `object`) | `PlacementOffering` with `shouldPresent` | `BillDog.purchase(activity, pkg)` → `PurchaseResult`; `BillDog.restorePurchases()` → `RestoreResult` | throws `BillDogException`, `error` is `BillDogError.PlacementNotFound` |\n| React Native, Expo | `BillDog.getOffering(placement)` | `OfferingResult`: `offering`, `presentation`, `placement` | `BillDog.purchasePackage(pkg)` and `BillDog.restore()` never throw; switch on `result.type` | rejects with `PlacementNotFoundError` |\n| Flutter | `BillDog.shared.getOffering(placement)` (never throws) | sealed `OfferingResult`: `OfferingAvailable`, `OfferingHoldout`, `OfferingWithheld`, `OfferingNotConfigured`, `OfferingFailed` | `BillDog.shared.purchasePackage(pkg)`; `BillDog.shared.restorePurchases()` | `OfferingFailed` whose `error` is a `PlacementNotFoundError` |\n| Web | `BillDog.getOffering(placement)` on the default export of `@billdog.io/web` | `OfferingResult`: `offering`, `presentation`, `placement` | `BillDog.purchaseProduct(productId)` hands the visitor to Stripe checkout; the purchase is recorded when they return | throws `PlacementNotFoundError` from `@billdog.io/core` |\n\nPlatform notes:\n\n- **iOS and Android:** `shouldPresent` already folds rule 3 in: it is true only for a\n non-null offering that is neither in a holdout nor withheld.\n- **React Native and Expo:** a package's price and name are `pkg.product?.priceString`\n and `pkg.product?.displayName` (there is no `storeProduct`, and no `status` field\n on the result). Expo is the React Native SDK re-exported and needs a development\n build; the native module is not in Expo Go. In a `useEffect`, keep the offering you\n showed in a local variable for the cleanup's `trackPaywallDismissed`, because the\n cleanup closes over the first render's state.\n- **Flutter:** switch on the sealed result; `OfferingAvailable` is the only case that\n draws.\n- **Web:** pass the product's `id` to `purchaseProduct`.\n\n## Without an SDK: REST\n\nFor Unity, Unreal, Godot, TV or desktop apps. Official Unity, Roku, Capacitor,\nCordova and Kotlin Multiplatform SDKs are not released yet, and Roku Pay purchases\nare not supported.\n\n1. **Device proof, once per install.** Generate and store with the app's secrets:\n - device id: `$BillDogAnonymousID:` followed by a random UUID;\n - device key: `bdk_` followed by 32 random bytes in base64url without padding\n (43 characters).\n \n Send them as `X-BillDog-Device` and `X-BillDog-Device-Key` on every request.\n BillDog binds the key the first time it sees the id. Never generate a new key for\n an id you already sent. A 401 `PROOF_INVALID` means the pair is malformed,\n unknown or revoked: make a new pair (a new anonymous customer, who gets purchases\n back by restoring).\n2. **Send the public key as `X-BillDog-API-Key` on every call.** A JSON body\n `api_key` is accepted as a fallback, and the header wins when both are sent.\n Never put the key in `Authorization: Bearer` or a `?api_key=` query parameter:\n neither is read.\n3. **Read the placement:**\n\n ```\n POST https://api.billdog.io/v1/get-wall-for-placement\n X-BillDog-API-Key: bd_public_…\n X-BillDog-Device: $BillDogAnonymousID:…\n X-BillDog-Device-Key: bdk_…\n\n { \"render_mode\": \"headless\", \"placement_identifier\": \"onboarding\",\n \"platform\": \"unity\" }\n ```\n\n `platform` is required: `ios`, `android`, `web`, `amazon`, `roku`, or `unity` for\n a player with no mobile store. The answer is `data.offering` (snake_case, packages\n with `product.app_store_product_id` / `play_store_product_id` / `stripe_price_id`),\n `data.presentation` and `data.paywall: null`.\n4. **Customer endpoints** (`process-purchase` for a store receipt from your own store\n code, `check-entitlement`, `get-customer-info`, `create-web-checkout`) need the\n device proof; without it they answer 403 `PROOF_REQUIRED`. Where a body asks for\n `app_user_id`, send the device id.\n5. **Impressions:** send `paywall_presented` and `paywall_dismissed` to\n `ingest-events` with `offering_id`, `offering_identifier` and\n `placement_identifier`.\n\nRequest and response bodies: https://billdog.io/docs/api-reference.\n\n## Check it works\n\n- `resolve_placement_preview` (MCP) shows what a client gets for a placement: the\n campaign, the offering and its packages.\n- After a test run, `get_recent_sdk_activity` should show `paywall_presented`. The\n billdog-analyst skill reads conversion once purchases arrive.\n"
}SHA-256 of public snapshot: e9430dc871d59b041c3c357533f21679b76823f9394ea5e67ce14b5b92c1ac43