# PriceOptimiser Rules API — frontend integration Base URL: `https://staging.priceoptimiser.thebesads.com/api/v1` This is an isolated staging system. Its MySQL database, Cloudflare D1 database, Worker and SDK are separate from existing production websites. GAM unit creation/management remains your frontend's responsibility. ## Authentication Call the API from your frontend's backend using `Authorization: Bearer `. Do not put the management key in publisher scripts or a publicly shipped frontend bundle. The SDK runtime endpoint is public and needs no management key. CORS is denied except for explicitly configured admin origins; ask to add a frontend origin if needed. Browser authentication/user management is not part of this API release. Every write requires a unique `Idempotency-Key` header. Retry the same request with the same key after a network failure. Reusing a key with different data returns 409. Updates also require `expectedVersion`, obtained from the latest GET; 409 means reload and reconcile, not blindly retry with a newer version. The downloadable `openapi.json` beside this guide describes all endpoints. An authenticated copy is available at `GET /openapi.json`; `GET /schema` provides strict JSON Schemas. Unknown fields are rejected. ## The simple form 1. `GET /templates` populates the baseline dropdown. 2. `GET /regions` supplies region labels and country membership. 3. Copy the selected template's `data.rules` into form state. Edit a row, remove a row, edit its prices, or select a waterfall depth locally. 4. Submit `POST /sites` once to save the website and its separate baseline experiment. This does not modify the template or publish ads. Example (all paths below are relative to the base URL): ```json { "website": "newpublisher.example", "templateId": "standard", "templateVersion": 1, "experimentName": "Baseline v1", "variantPercentage": 80 } ``` Supply `rules` in the same request to save edited/deleted rows. Omit it to copy every template row. `variantPercentage` is **0–100**, not 0–1; omitted uses the template default. Zero and 100 are valid. The saved experiment has its own ID and assignment group. The Standard baseline contains Native, Anchor, Interstitial and Rewarded: Tier 1 `[8,5,3,1]`, worldwide fallback `[3,1]`, 80% variant/20% unpriced control. Tier 1 is GB, US, AU, CA. ROW is represented by omitted geography: the narrower Tier 1 rule wins for its countries. No legacy country map is used. A numbered scale is a form interaction, not a separate API property: selecting depth 2 from `[8,5,3,1]` sends `prices: [8,5]`. The number of attempts is derived from the saved array. One unpriced fallback is additional. Retain the unselected choices locally if you want to let the user expand the scale before saving. ## Rules ```json { "id": "native-tier1", "target": {"format": "native"}, "geography": {"regions": ["tier1"], "countries": ["SG"]}, "devices": ["mobile"], "prices": [8,5,3,1], "fallback": true } ``` Choose `target.format` OR `target.adUnitPath`, never both. Specific-unit rules must reference registered inventory. Omitted geography/device means all. Countries and selected regions form a union with duplicates removed. Use canonical ISO codes; GB is UK. Country lists and region versions are resolved and frozen when a website revision is saved. Updating a catalog region does not rewrite saved/published website revisions; resave a website configuration to adopt current region membership. Most-specific coverage wins. Cross-dimension conflicts return 422 `AMBIGUOUS_RULES` with an example country, device and unit, plus conflicting rule/experiment IDs. Adding a genuinely narrower intersection rule can resolve the overlap. An arbitrary filled-field count is not used. Maximum 200 rules total per website. Edit all fields through `PUT /sites/{site}/configuration`, or edit one experiment with `PUT /sites/{site}/experiments/{id}` and `{expectedVersion, experiment}`. To delete a rule, remove it from that experiment's rules array and save. To delete the whole experiment, `DELETE /sites/{site}/experiments/{id}` with `{expectedVersion}`. Previous versions remain in history. Template copies added to existing sites through `POST /sites/{site}/experiments` start disabled to avoid competing with active rules. ## Publisher DIV IDs and inventory `PUT /sites/{site}/inventory` takes a complete inventory/mapping replacement: ```json { "expectedVersion": 1, "inventory": [ {"id":"native-desktop","format":"native","path":"/123456789/newpublisher.example_Native","enabled":true}, {"id":"native-mobile","format":"native","path":"/123456789/newpublisher.example_Native_m","enabled":true} ], "slots": [ { "id":"incontent", "divId":"publisher-ad-incontent", "format":"native", "enabled":true, "adUnits":{"desktop":"native-desktop","mobile":"native-mobile"}, "sizes":[[300,250],[336,280]] } ] } ``` `divId` is the publisher's actual HTML ID. The SDK uses it unchanged for both devices. `id` is the stable SDK slot key used in preload/reveal calls; it is not an experiment dimension. Reuse an inventory unit across multiple slots if needed. Use one slot with device mappings for each unique DIV. Tablet falls back to desktop unless a tablet mapping is explicitly supplied; omitted mobile mapping also falls back to desktop. Rewarded/Interstitial use `divId: null` and `sizes: []` for out-of-page formats. ## Advanced timing Omit `timing` to use defaults: 0 ms delay after a confirmed empty result, 2500 ms rewarded no-response deadline, and an overall deadline calculated from the longest ladder with a minimum 15000 ms. Overrides use `timing.retryDelayMs` (must be 0), `timing.noResponseTimeoutMs`, and `timing.overallTimeoutMs`. Current SDK distinction: rewarded has a per-attempt no-response timer; display uses the overall deadline when no render result arrives. A confirmed display no-fill advances immediately. The per-attempt timer must not be presented as an artificial wait after a known no-fill. Native/Anchor support `refresh: {intervalSeconds: 30, ladders: [[5,3,1],[8,5,3,1]]}`. Control remains unpriced. Shared `assignmentGroup` IDs reuse assignment across experiments and require matching splits. ## Validate, preview, publish and rollback - `POST /sites/{site}/validate` with `{configuration}` checks unsaved edits. - `POST /sites/{site}/preview` with `{version, context:{country:"US",device:"mobile",adUnitPath:"/123456789/newpublisher.example_Native_m"}}` returns the matched rules, winner and actual ladder. It makes no GAM request. - `POST /sites/{site}/publish` with `{version:2}` returns 202 and a publication ID. Poll `GET /publications/{id}` until `published` or `failed`. A saved revision is not live until publication is verified in D1. - `POST /sites/{site}/rollback` with a prior version publishes that exact saved snapshot using a new fenced publication ID. - `GET /sites/{site}/history` returns the latest 100 revisions with actors and timestamps. `GET /sites/{site}/revisions/{version}` returns the configuration. - `GET /sites/{site}/publisher-instructions` returns the published script URL and instructions. Add `?version=2` for an explicitly labelled draft, or `&format=markdown` for Markdown. The public serving URL is `https://priceoptimiser-rules-staging.adesh-4df.workers.dev/runtime/config?site=`. Use the `runtimeUrl` returned by publisher instructions, rather than constructing it from the API hostname. Country comes from Cloudflare, device from user agent. Country/device query overrides are ignored. `/sdk/publisher-sdk.js` is an isolated SDK build using the existing ad lifecycle with assignment-group persistence added. ## Release boundaries - No existing websites have been imported or migrated. - Video can be stored in inventory/rules, but publication of enabled Video slots is blocked until its adapter is integrated. - Scheduled day/time rules are intentionally not accepted yet; no schedule is silently ignored. - This SDK release requires an unpriced fallback; configurations disabling it cannot be published yet. - Telemetry/revenue reporting is not enabled in this isolated release. Validate the first real publisher and GAM pricing setup before production traffic. - The inline conversation mockup remains a mockup. Your frontend must call these endpoints; it must not write MySQL or D1 directly.