API reference.

At a glance

REST over HTTPS, JSON only, bearer-authenticated. See Conventions for versioning, idempotency, errors, pagination, and rate limits. Sample tokens below are placeholders, mint your own from your VERSE workspace.

Base URL https://api.verse.ad/v1Auth Bearer sk_live_...Rate limit 600 req / min defaultBurst 200 req / sec

Surfaces

The API groups into ten surfaces. Each surface scopes by token; an Ads Manager token cannot read screens, a Screen Manager token cannot launch campaigns. Agency tokens can act on the sub-accounts they own.

SurfaceAudienceRoot
CampaignsBrands, agencies/campaigns
CreativesBrands, agencies/creatives
ForecastsBrands, agencies/forecasts
ReportingBrands, agencies/reporting
ScreensVenues/screens
PairingVenues/pairing
PlaylistsVenues/playlists
PayoutsVenues/payouts
Sub-accountsAgencies/agency/clients
Audit logAll/audit

Authentication

Every request carries an Authorization: Bearer sk_live_... header. Tokens are issued from your VERSE workspace under Settings → API keys. Read-write tokens can run any endpoint allowed by the workspace role; read-only tokens can only call GET. The sk_test_... prefix routes to the sandbox.

curl https://api.verse.ad/v1/me \
  -H "Authorization: Bearer sk_live_4f3c8b2a..."
Token hygiene

Tokens are scoped to one workspace. Mint a separate token per integration so a leaked DSP key cannot drain a wallet. Rotate every 90 days; rotation is non-disruptive (both keys are valid for a 24h overlap).

Ads Manager · Brands and agencies

Campaigns and creative

Campaigns

A campaign is a brief, a budget, a schedule, a target plan, and a set of creatives. Once approved, campaigns deliver impressions across paired screens that match the targeting filter. Brands and agencies use this surface; agencies prefix paths with the sub-account workspace via the X-Workspace-Id header.

GET/campaigns

List campaigns in this workspace, newest first. Filter with status, category, or scheduling window. Supports cursor pagination.

Request
curl https://api.verse.ad/v1/campaigns?status=live&limit=20 \
  -H "Authorization: Bearer sk_live_..."
Response
{
  "data": [
    {
      "id": "cmp_8x29f1",
      "name": "Iced Macchiato, Spring",
      "status": "live",
      "budget": { "amount": 1500.00, "currency": "GBP" },
      "spent":  { "amount":  421.18, "currency": "GBP" },
      "starts": "2026-04-29T00:00:00Z",
      "ends":   "2026-05-27T23:59:59Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

POST/campaigns

Create a new campaign. Returns the full campaign with status pending_review until it clears brand-safety checks (typically under five minutes).

Request
curl -X POST https://api.verse.ad/v1/campaigns \
  -H "Authorization: Bearer sk_live_..." \
  -H "Idempotency-Key: 8c2b7e1c-..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Iced Macchiato, Spring",
    "category": "f-and-b",
    "budget": { "amount": 1500.00, "currency": "GBP" },
    "targets": {
      "cities": ["london", "manchester"],
      "venue_types": ["cafe", "restaurant"],
      "daypart": { "start": "06:00", "end": "11:00" }
    },
    "creatives": ["cre_8x29f1"],
    "schedule": { "starts": "2026-04-29", "ends": "2026-05-27" }
  }'
Response
{
  "id": "cmp_8x29f1",
  "status": "pending_review",
  "rate": { "amount": 0.0061, "currency": "GBP", "unit": "impression" },
  "forecast": {
    "impressions_low":  205000,
    "impressions_high": 248000,
    "scan_rate_est":    0.0034
  }
}

GET/campaigns/{id}

Fetch a single campaign with delivery counters, current pacing, and creative bindings.

PATCH/campaigns/{id}

Update budget, schedule, targets, creative weights, or status. Live edits propagate to delivery within minutes; pause and resume are instant.

Request
curl -X PATCH https://api.verse.ad/v1/campaigns/cmp_8x29f1 \
  -H "Authorization: Bearer sk_live_..." \
  -H "Idempotency-Key: ..." \
  -d '{ "status": "paused" }'

DELETE/campaigns/{id}

Soft-delete a campaign. Live campaigns are paused first. Refunds any unspent reserved budget to the wallet.

Creatives

Creatives are 6 to 15 second video assets, or static stills with optional audio. The POST endpoint returns a signed URL; upload the binary directly to it. We transcode for the eight player platforms automatically.

GET/creatives

List creatives in this workspace. Filter by status (draft, ready, rejected) and orientation.

POST/creatives

Reserve a creative slot and get a signed upload URL. The asset becomes status:ready once transcoding completes (typically under two minutes).

Request
curl -X POST https://api.verse.ad/v1/creatives \
  -H "Authorization: Bearer sk_live_..." \
  -d '{
    "name":        "Iced Macchiato 9x16 v2",
    "orientation": "portrait",
    "duration_ms": 8000,
    "filename":    "iced-macchiato-9x16-v2.mp4"
  }'
Response
{
  "id": "cre_8x29f1",
  "status": "awaiting_upload",
  "upload_url": "https://up.verse.ad/...",
  "expires_at": "2026-05-05T18:42:00Z"
}

DELETE/creatives/{id}

Detach a creative. Live campaigns referencing it pause unless other creatives remain in rotation.

Forecasts

Forecasts return servable impressions, scan-rate estimates, and a pacing curve for a brief, a budget, and a target plan. Cheap to call, idempotent, capped at 50 forecasts per minute per token. Use them to validate inputs before POST /campaigns.

POST/forecasts

Run a forecast. Returns a low and high impression band (90% confidence), an estimated scan rate, and a daily pacing curve.

Request
curl -X POST https://api.verse.ad/v1/forecasts \
  -H "Authorization: Bearer sk_live_..." \
  -d '{
    "budget":      { "amount": 1500, "currency": "GBP" },
    "venue_types": ["cafe", "gym"],
    "cities":      ["london"],
    "schedule":    { "starts": "2026-05-12", "ends": "2026-05-26" },
    "daypart":     { "start": "07:00", "end": "10:00" }
  }'
Response
{
  "rate":              { "amount": 0.0061, "currency": "GBP" },
  "impressions_low":   198400,
  "impressions_high":  241800,
  "scan_rate_est":     0.0034,
  "screens_eligible":  642,
  "fill_probability":  0.91,
  "pacing": [
    { "date": "2026-05-12", "impressions_est": 13700 },
    { "date": "2026-05-13", "impressions_est": 14100 }
  ]
}

Reporting

Reporting endpoints aggregate delivery, scan, and brand-lift data. Cursor-paginated; results are eventually consistent within 90 seconds of delivery. For agency cross-roster reports use /agency/reports below.

GET/reporting/campaigns/{id}

Aggregate delivery for a single campaign. Filter by daypart, venue, or city. Group by day or hour.

Request
curl "https://api.verse.ad/v1/reporting/campaigns/cmp_8x29f1?from=2026-04-29&to=2026-05-05&group=day" \
  -H "Authorization: Bearer sk_live_..."
Response
{
  "campaign_id": "cmp_8x29f1",
  "totals": {
    "impressions": 75200,
    "scans":          258,
    "scan_rate":   0.00343,
    "spend": { "amount": 458.72, "currency": "GBP" }
  },
  "series": [
    { "bucket": "2026-04-29", "impressions": 10200, "scans": 32 },
    { "bucket": "2026-04-30", "impressions": 11400, "scans": 41 }
  ]
}

GET/reporting/scans

Per-scan event log. Each scan ties back to a screen, a venue, a daypart, and the creative variant rendered. Useful for downstream attribution warehouses.

GET/reporting/brand-lift

Brand-lift survey results for campaigns where opt-in was enabled. Returns awareness, recall, and intent deltas with sample sizes.

Screen Manager · Venues

Screens, pairing, playlists, payouts

Screens

A screen is a paired display running a VERSE player on Android TV, Samsung Tizen, LG webOS, Amazon Fire TV, Google TV, Windows, Linux, or any modern browser. Each screen belongs to a venue and runs one playlist at a time.

GET/screens

List screens. Filter by venue, status (online, offline, paired_idle), or platform.

Response
{
  "data": [
    {
      "id": "scr_8x29f1",
      "venue_id": "ven_3a14b2",
      "label":    "Front counter",
      "platform": "tizen",
      "status":   "online",
      "last_seen_at": "2026-05-05T17:51:09Z",
      "sense_paired": true
    }
  ]
}

POST/screens

Register a screen by claiming a freshly minted pairing code. Equivalent to typing the 6-digit code on the device.

Request
curl -X POST https://api.verse.ad/v1/screens \
  -H "Authorization: Bearer sk_live_..." \
  -d '{
    "venue_id": "ven_3a14b2",
    "label":    "Front counter",
    "code":     "482917"
  }'

PATCH/screens/{id}

Update screen label, venue assignment, ad-fill ratio, or playlist binding.

DELETE/screens/{id}

Unpair a screen. The device returns to its blank pairing prompt within seconds.

Pairing

Pairing mints a 6-digit code valid for 60 seconds. The device polls for claim; the dashboard or this API confirms it. Eight platforms supported, browser-based, no app store required.

POST/pairing

Mint a pairing code. Returns the code and an expiry timestamp.

Request
curl -X POST https://api.verse.ad/v1/pairing \
  -H "Authorization: Bearer sk_live_..." \
  -d '{ "venue_id": "ven_3a14b2" }'
Response
{
  "code":       "482917",
  "expires_at": "2026-05-05T17:53:00Z",
  "claim_url":  "https://hub.verse.ad/pair/482917"
}

GET/pairing/{code}

Poll the pairing status. Returns claimed:true and the new screen id once a device confirms.

Playlists

A playlist is an ordered list of items: AI-generated menus, brand creatives, sponsorship cards, and venue-owned content. The runtime weaves ad-fill in based on the screen’s ad-ratio setting (0% to 100%).

GET/playlists

List playlists in this venue or workspace.

POST/playlists

Create a playlist. Items can reference creative ids, AI menu templates, or static images.

Request
curl -X POST https://api.verse.ad/v1/playlists \
  -H "Authorization: Bearer sk_live_..." \
  -d '{
    "name": "Lunch service",
    "items": [
      { "type": "ai_menu", "template": "lunch-grid", "duration_ms": 12000 },
      { "type": "image",   "asset_id":  "ast_2f1c8b", "duration_ms":  5000 },
      { "type": "ad_slot", "weight":    1.0 }
    ]
  }'

PATCH/playlists/{id}

Reorder items, change durations, swap assets, or update the ad-slot weighting.

DELETE/playlists/{id}

Archive a playlist. Active screens fall back to the venue default.

Payouts

Venue earnings are calculated daily and withdrawable on demand. Up to 50% of ad revenue when the venue runs ad fill at the maximum tier with a paired VERSE Sense module (£20, shipping included). Without Sense, commission is shaped by daily visitors, dwell, and ad ratio. Withdrawals over £200 are free; a small standard sending fee applies below.

GET/payouts

List payout withdrawals for this workspace. Each payout itemises which venues contributed and the gross-to-net breakdown.

Response
{
  "data": [
    {
      "id":       "pay_2026_04",
      "period":   "2026-04",
      "status":   "paid",
      "amount":   { "value": 2841.06, "currency": "GBP" },
      "paid_at":  "2026-05-03T09:14:00Z",
      "venues_count": 4
    }
  ]
}

GET/payouts/{id}

Fetch a single payout with per-venue line items, impression counts, and the commission tier applied.

Agency Suite · Agencies

Sub-accounts and consolidated reporting

Sub-accounts

Each agency client lives in its own ring-fenced workspace, scoped wallets, scoped roles, scoped audit log. Agency tokens can act on any sub-account by sending the X-Workspace-Id: ws_... header. Agencies earn up to 30% off platform CPM (volume tier up to 22%, slot fill bonus up to 6%, case-study opt-in up to 2%).

GET/agency/clients

List sub-accounts under this agency. Includes wallet balances, MTD spend, and the live discount tier per client.

Response
{
  "data": [
    {
      "id": "ws_velvet_ridge",
      "name": "Velvet Ridge",
      "wallet": { "balance": { "amount": 4180.00, "currency": "GBP" } },
      "mtd_spend":     { "amount": 5840.20, "currency": "GBP" },
      "discount_tier": 0.20
    }
  ]
}

POST/agency/clients

Spin up a new sub-account. Defaults to inherit-from-agency for branding, audit retention, and approval workflow.

Request
curl -X POST https://api.verse.ad/v1/agency/clients \
  -H "Authorization: Bearer sk_live_..." \
  -d '{
    "name": "Pulse Gym",
    "billing_model": "pass-through",
    "currency": "GBP"
  }'

PATCH/agency/clients/{id}

Update a sub-account, name, billing model, branding, or seat list.

GET/agency/reports

Cross-roster delivery, spend, and savings. Group by client, region, daypart, or campaign.

Request
curl "https://api.verse.ad/v1/agency/reports?from=2026-04-01&to=2026-04-30&group=client" \
  -H "Authorization: Bearer sk_live_..."
Response
{
  "totals": {
    "platform_cost": { "amount": 18405.00, "currency": "GBP" },
    "discount":      { "amount":  3681.00, "currency": "GBP" },
    "net_cost":      { "amount": 14724.00, "currency": "GBP" }
  },
  "by_client": [
    { "client_id": "ws_velvet_ridge",    "spend": 4120.10 },
    { "client_id": "ws_peak_performance","spend": 3680.40 },
    { "client_id": "ws_pulse_gym",       "spend": 3210.55 }
  ]
}

GET/invoices

List invoices for the agency or a sub-account. PDFs are returned as signed URLs valid for 24 hours.

Reference · All audiences

Audit log

Every state change in a workspace appends to the audit log: who, what, when, from where (IP and user agent), with a before and after diff. Retained for seven years on Scale and Enterprise plans, two years otherwise. Read-only via the API; rotate with /audit/export for warehouse loads.

GET/audit

List audit entries. Filter by actor, entity (campaign, screen, payout), action, or time range.

Response
{
  "data": [
    {
      "id":       "aud_8x29f1",
      "at":       "2026-05-05T17:42:18Z",
      "actor":    { "type": "user", "id": "usr_3a14b2", "email": "ops@velvetridge.co" },
      "action":   "campaign.paused",
      "entity":   { "type": "campaign", "id": "cmp_8x29f1" },
      "ip":       "82.14.21.7",
      "diff":     { "status": { "from": "live", "to": "paused" } }
    }
  ]
}

POST/audit/export

Kick off an async export of audit entries to S3, GCS, or a signed CSV URL. Returns a job id; poll with /audit/export/{job_id}.

Rate limits

Default is 600 requests per minute per token, with a burst capacity of 200 requests per second. Forecasts and audit exports are 50 per minute per token. Higher limits are raised on request, write to hello@verse.ad with a one-line description of the integration.

HeaderDescription
X-RateLimit-LimitThe ceiling for the bucket on this token (e.g. 600).
X-RateLimit-RemainingCalls left in the current minute.
X-RateLimit-ResetSeconds until the bucket refills.
Retry-AfterSent only on 429. Wait this many seconds before retrying.

Webhooks

Subscribe to events for campaigns, creatives, screens, scans, payouts, and audit entries. We POST a signed JSON payload to your endpoint and retry with exponential backoff for up to 24 hours. Endpoints must respond with a 2xx within 5 seconds.

Subscribing

POST/webhooks

Register a webhook endpoint. Returns a signing secret used to verify every payload.

Request
curl -X POST https://api.verse.ad/v1/webhooks \
  -H "Authorization: Bearer sk_live_..." \
  -d '{
    "url":    "https://yourapp.example/verse-events",
    "events": ["campaign.approved", "scan.recorded", "payout.paid"]
  }'
Response
{
  "id":       "whk_8x29f1",
  "url":      "https://yourapp.example/verse-events",
  "secret":   "whsec_4f3c8b2a...",
  "events":   ["campaign.approved", "scan.recorded", "payout.paid"],
  "status":   "active"
}

Event types

  • campaign.created, campaign.approved, campaign.paused, campaign.resumed, campaign.finished, campaign.rejected
  • creative.uploaded, creative.ready, creative.rejected
  • screen.paired, screen.online, screen.offline, screen.unpaired
  • scan.recorded (with screen, venue, and daypart attribution)
  • payout.issued, payout.paid, payout.failed
  • invoice.issued, invoice.paid
  • audit.entry.created

Verifying signatures

Every payload carries an X-Verse-Signature header in the form t=<timestamp>,v1=<hex>. Compute HMAC-SHA256(secret, "{timestamp}.{raw_body}") and constant-time compare. Reject anything older than 5 minutes to prevent replay attacks.

// Node.js, untrusted body parser, raw body required
import crypto from "crypto";

function verify(rawBody, header, secret) {
  const [tPart, vPart] = header.split(",");
  const t = tPart.split("=")[1];
  const sig = vPart.split("=")[1];
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
  return fresh && crypto.timingSafeEqual(
    Buffer.from(sig, "hex"),
    Buffer.from(expected, "hex"),
  );
}

Retries and idempotency

Failed deliveries (non-2xx, timeout) retry on a backoff: 30s, 1m, 5m, 30m, 2h, 6h, 12h, 24h. Each retry carries the same id, deduplicate by it on your side. Endpoints that flap to 4xx (auth or schema) are auto-disabled after 24 consecutive failures and surface in the dashboard.

SDKs

Official SDKs in TypeScript, Python, and Go are in active drafting. Until they ship, the API is plain HTTP / JSON and trivially usable from any language. The X-Verse-Sdk header on requests helps us prioritise: send a string like my-app/1.4.2 if you want it counted.

Status

Surface coverage is complete. Per-endpoint deep-dives (every field, every error, every edge case) ship behind the Soon sidebar links as each section is finalised. For anything not yet documented, hello@verse.ad reaches an engineer the same day.