Appearance
WARNING
Treat API keys like passwords. Do not commit them to git or paste them in public channels.
Platform overview
Pulseflow is the live bidding and pacing service: it reads your campaign configuration, applies spend and pacing rules, works with the DSP bidder on OpenRTB traffic from SSPs (supply-side platforms / exchanges), and writes billing and play records into the archive (used for the play-data endpoints in this guide).
The Vue API (the API_HOST in the examples below) is the control plane for inventory and campaigns: sign catalog, partner keys, create/update/activate campaigns, and links to clients. It stores campaign documents that Pulseflow and related services consume—it is not a substitute for Pulseflow in the auction path.
Test requests with your API key
There is no built-in “Try it” code panel inside these docs right now. Safe ways to exercise the API:
- Terminal + curl — Copy the examples in this page; export
API_HOSTandDSP_API_KEYfirst (see below). Best for quick checks. - Postman, Insomnia, or VS Code REST Client — Create an environment with
API_HOSTandDSP_API_KEY. For partner routes, set headerAuthorizationtoBearerorX-Vue-Dsp-Api-Key:. - Your own app or script — Same headers from any HTTP client in the language you use.
Avoid pasting production keys into random public “API tester” websites or shared online consoles.
If you need an in-browser explorer later, options include publishing an OpenAPI description of these routes and hosting Swagger UI or Scalar against a staging environment only.
Set these once (replace with your real host and key):
bash
export API_HOST="https://api.vuebillboards.com"
export DSP_API_KEY="vbdsp.<keyId>.<secret>"Your Vue Billboards contact issues the DSP_API_KEY. It is tied to your DSP account; if that account changes, the key may stop working.
Send the key on every partner request using either:
Authorization: Bearer $DSP_API_KEY, orX-Vue-Dsp-Api-Key: $DSP_API_KEY
Suggested order: search inventory first (calls below use API_HOST only — no partner key). From each screen you want, copy adunit_id or programmatic_platform_key; those strings are what you send as platformKeys when you create a campaign. After that, use DSP_API_KEY for create, get, edit, activate, and PulseFlow Bid Archive billing reads (see Play and spend data (PulseFlow Bid Archive)).
Look up inventory (screens)
Sign record shape (reference)
Real rows vary by publisher and import source; many fields can be null or missing. Typical keys you need for campaigns are adunit_id, programmatic_platform_key, and sometimes openRTBUUID.
json
{
"id": "internal-row-id",
"asset_name": "UNIT-5021-NYC",
"adunit_id": "hs-screen-adunit-abc123",
"programmatic_platform_key": "hivestack-platform-key-xyz",
"openRTBUUID": "550e8400-e29b-41d4-a716-446655440000",
"openrtb_uuid": null,
"screen_id": "SCR-8899",
"screen_name": "7th Ave @ 42nd St",
"name": "Times Square Digital",
"adunit_name": "TSQ-LED-01",
"address": "1 Times Square",
"city": "New York",
"state": "NY",
"region": "New York",
"latitude": 40.758,
"longitude": -73.9855,
"format": "2880x960",
"width": 2880,
"height": 960,
"preferred_ad_format": "2880x960",
"media_type": "Digital Billboard",
"media_type_id": "101",
"seller_id": "hs-na-1234",
"publisher": "Example SSP Name",
"media_owner": "Example Media Owner",
"floorCpm": 18.5,
"status": "active",
"asset_image_url": "https://cdn.example.com/preview.jpg",
"markets": "New York NY",
"dma": "501"
}By latitude, longitude, and radius (recommended)
GET /api/signs
Returns every screen whose latitude / longitude falls within a circle around your point.
| Query | Required | Description |
|---|---|---|
lat | Yes | Center latitude (decimal degrees, WGS84) |
lng | Yes | Center longitude (decimal degrees, WGS84) |
radius | Yes | Radius in kilometers (not miles) |
seller_id | No | Keep only signs for this seller / publisher id |
includeperion | No | 1 to include inventory that is otherwise gated for some publishers |
includelinknyc | No | 1 to include extra LinkNYC-related units in NYC |
includeoutfrontrestrictedmediatypes | No | 1 for “advanced” inventory (additional Outfront / restricted media types) |
Miles → km: multiply miles by 1.609 (e.g. 5 miles ≈ 8.05 km).
curl — 15 km around Midtown Manhattan
bash
curl -sS "$API_HOST/api/signs?lat=40.7580&lng=-73.9855&radius=15"Success (200)
json
{
"success": true,
"data": {
"signs": [
{
"id": "internal-row-id",
"asset_name": "UNIT-5021-NYC",
"adunit_id": "hs-screen-adunit-abc123",
"programmatic_platform_key": "hivestack-platform-key-xyz",
"openRTBUUID": "550e8400-e29b-41d4-a716-446655440000",
"latitude": 40.758,
"longitude": -73.9855,
"address": "1 Times Square",
"city": "New York",
"seller_id": "hs-na-1234",
"publisher": "Example SSP Name",
"format": "2880x960",
"floorCpm": 18.5,
"asset_image_url": "https://cdn.example.com/preview.jpg"
}
],
"total": 1,
"radius": 15,
"center": { "lat": 40.758, "lng": -73.9855 }
}
}Errors
| HTTP | Example body |
|---|---|
| 400 | { "error": "Missing required parameters: lat, lng, radius" } |
| 400 | { "error": "Invalid parameters: lat, lng, and radius must be valid numbers" } |
One screen by id / name / uuid
GET /api/signs/match
Use when you already have a single id string (asset name, ad unit id, platform key, or OpenRTB UUID).
curl — exact match on a known unit id
bash
curl -sS "$API_HOST/api/signs/match?asset_name=hs-screen-adunit-abc123&fuzzy=false"Success (200) — data is one sign object (same field idea as the reference above).
json
{
"success": true,
"data": {
"id": "internal-row-id",
"asset_name": "UNIT-5021-NYC",
"adunit_id": "hs-screen-adunit-abc123",
"programmatic_platform_key": "hivestack-platform-key-xyz",
"openRTBUUID": "550e8400-e29b-41d4-a716-446655440000",
"latitude": 40.758,
"longitude": -73.9855,
"address": "1 Times Square",
"city": "New York",
"seller_id": "hs-na-1234",
"publisher": "Example SSP Name",
"format": "2880x960",
"floorCpm": 18.5,
"asset_image_url": "https://cdn.example.com/preview.jpg"
}
}Not found (404)
json
{
"error": "No sign found for asset_name",
"asset_name": "hs-screen-adunit-abc123",
"availableFields": [
"asset_name",
"adunit_name",
"screen_id",
"screen_name",
"id",
"adunit_id",
"programmatic_platform_key",
"openRTBUUID",
"openrtb_uuid",
"openRtbUuid"
],
"totalSigns": 12345
}Add fuzzy=true for partial / substring-style matching.
Many screens at once
GET /api/signs/match/bulk
bash
curl -sS "$API_HOST/api/signs/match/bulk?asset_names=hs-screen-adunit-abc,key-two,key-three"Success (200) — each data is either a full sign object or null.
json
{
"success": true,
"data": [
{
"asset_name": "hs-screen-adunit-abc",
"found": true,
"data": {
"adunit_id": "hs-screen-adunit-abc",
"programmatic_platform_key": "hivestack-platform-key-xyz",
"latitude": 40.758,
"longitude": -73.9855
}
},
{
"asset_name": "key-two",
"found": false,
"data": null
}
],
"summary": { "total": 2, "found": 1, "notFound": 1 }
}By platform keys (batch)
POST /api/signs/by-platform-keys
bash
curl -sS -X POST "$API_HOST/api/signs/by-platform-keys" \
-H "Content-Type: application/json" \
-d '{"platformKeys":["KEY1","KEY2"]}'Success (200)
json
{
"success": true,
"data": [
{
"adunit_id": "KEY1",
"programmatic_platform_key": "KEY1",
"latitude": 25.7617,
"longitude": -80.1918,
"asset_name": "MIA-001"
}
]
}By seller id
GET /api/signs/by-seller-id
bash
curl -sS "$API_HOST/api/signs/by-seller-id?seller_id=hs-na-1234"Success (200)
json
{
"success": true,
"seller_id": "hs-na-1234",
"total": 2,
"data": [ { "seller_id": "hs-na-1234", "adunit_id": "..." } ]
}Create a campaign
Use your DSP_API_KEY on this request.
POST /api/partner/v1/campaigns
curl
bash
curl -sS -X POST "$API_HOST/api/partner/v1/campaigns" \
-H "Authorization: Bearer $DSP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Spring promo",
"description": "Optional",
"clientSeatId": "my-brand-seat",
"startDate": "2026-04-01",
"endDate": "2026-04-30",
"totalBudget": 5000,
"maxCPM": 12.5,
"weeklyTimeSegments": [
{ "day": 1, "startTime": "09:00", "endTime": "17:00" },
{ "day": 2, "startTime": "09:00", "endTime": "17:00" }
],
"platformKeys": ["SCREEN_OR_ADUNIT_KEY_1", "SCREEN_OR_ADUNIT_KEY_2"]
}'Replace SCREEN_OR_ADUNIT_KEY_* with values from the inventory responses (adunit_id or programmatic_platform_key).
Required JSON fields
| Field | Type | Notes |
|---|---|---|
name | string | Campaign name |
clientSeatId | string | Your client / seat id (must already exist on your account) |
startDate, endDate | string | ISO dates |
totalBudget | number | Must be > 0 |
maxCPM | number | Must be > 0 |
weeklyTimeSegments | array | Each item: day (1=Mon … 7=Sun), startTime, endTime (HH:MM) |
platformKeys or signs | array | At least one screen — see below |
Screens — two styles
Simple list of ids (matched the same way as the map):
"platformKeys": ["key1", "key2"]Per-screen daily plays (default plays per day is 300 if omitted):
"signs": [ { "platformKey": "key1", "dailyPlays": 400 }, { "platformKey": "key2" } ]
Optional JSON fields
description, defaultDailyPlays, perSignBudgetEnabled, ungroupedSignBudgets, dealIds, deals, dealSelectionMode, signScheduleOverrides, linkToClient (default true links the campaign to the client record).
Success response (201)
json
{
"success": true,
"campaignId": "generated_campaign_id",
"dspInstanceId": "yourInstanceId",
"message": "Campaign created. Pre-submit creatives via the partner API or the web app, then call activation when ready."
}Typical errors
| HTTP | Body (example) |
|---|---|
| 400 | { "success": false, "error": "Sign not found for platform key: ..." } |
| 401 | { "success": false, "error": "Missing partner API key ..." } |
| 403 | { "success": false, "error": "API key disabled" } |
After creation, pre-submit creatives (below) or attach existing creative document ids, then call activation when ready.
Pre-submit a creative (Hivestack)
Registers a creative with Hivestack through perion-api-bridge (same path as the dashboard) and creates a Firestore creatives document you can reference from PATCH .../campaigns/:id (creativeDocIds / assignedCreatives).
IAB categories required
iab_categories is required on POST /api/partner/v1/creatives (non-empty array). Use valid codes from the taxonomy, e.g. the Kayzen IAB category list.
POST /api/partner/v1/creatives
| JSON field | Required | Description |
|---|---|---|
name | Yes | Creative name (max 100 chars sent upstream) |
advertiserDomain | Yes | Advertiser domain string (Hivestack) |
publisherId | Yes | Seller / publisher id for approval (e.g. HS-NA-123; seller_id accepted as alias) |
mediaUrl | Yes | Public HTTPS URL to the asset (image, video, zip, or html) — must be fetchable by Hivestack |
width, height | Yes | Pixel dimensions |
campaignId | No | If set, campaign must belong to this partner key; used to build the default VUE-... dsp creative id |
formatKey | No | Suffix in generated dsp id (e.g. sign format key) |
creativeId | No | Provide to reuse a fixed dsp id; otherwise generated like the web app |
mediaType | No | image, video, or html (default inferred from mediaUrl extension) |
format / mimeFormat | No | MIME type for media_files[] (default inferred from extension) |
iab_categories | Yes | Required. Non-empty array of IAB Content Category codes (e.g. IAB3, IAB19-1). Each entry must look like IAB + digits, with optional - segments (e.g. IAB2-10). The API returns 400 if this field is missing or empty. |
dealId | No | Optional deal id passed through to the bridge |
Use valid codes from the taxonomy (same family as Hivestack / industry lists), for example the Kayzen IAB category reference.
bash
curl -sS -X POST "$API_HOST/api/partner/v1/creatives" \
-H "Authorization: Bearer $DSP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Partner line 1",
"advertiserDomain": "example.com",
"publisherId": "HS-NA-123",
"mediaUrl": "https://cdn.example.com/creatives/line1-1920x1080.jpg",
"width": 1920,
"height": 1080,
"campaignId": "OPTIONAL_CAMPAIGN_ID",
"iab_categories": ["IAB3"]
}'Success (201)
json
{
"success": true,
"creativeDocId": "firestore_creatives_doc_id",
"dspCreativeId": "VUE-...",
"publisherId": "HS-NA-123",
"bridge": { },
"message": "Creative submitted to Hivestack via perion-api-bridge. Poll GET ... for approval status."
}Poll approval state (returns the bridge / Hivestack payload — same endpoint the web app polls):
GET /api/partner/v1/creatives/:dspCreativeId?publisherId=HS-NA-123
bash
curl -sS "$API_HOST/api/partner/v1/creatives/VUE-YOUR_DSP_ID" \
-H "Authorization: Bearer $DSP_API_KEY" \
-G --data-urlencode "publisherId=HS-NA-123"Interpret approval_state / publisher_approval_state like the dashboard: 1 pending, 2 approved, 3 rejected (see NewCampaignDetailView polling logic).
Retrieve a specific campaign
Returns the stored campaign document (budget, signs, deals, creative links, etc.) for one campaign id.
GET /api/partner/v1/campaigns/:campaignId
bash
curl -sS "$API_HOST/api/partner/v1/campaigns/CAMPAIGN_ID" \
-H "Authorization: Bearer $DSP_API_KEY"Success (200)
json
{
"success": true,
"campaignId": "CAMPAIGN_ID",
"data": { "...": "stored campaign fields" }
}Errors: 401 bad key, 403 not your campaign, 404 unknown id.
Edit a campaign
PATCH /api/partner/v1/campaigns/:campaignId
Send only fields you want to change.
curl (rename + budget)
bash
curl -sS -X PATCH "$API_HOST/api/partner/v1/campaigns/CAMPAIGN_ID" \
-H "Authorization: Bearer $DSP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Updated name",
"totalBudget": 6000,
"maxCPM": 15
}'Other supported fields (all optional in the patch body)
| Field | Purpose |
|---|---|
description | Text |
startDate, endDate | ISO strings |
weeklyTimeSegments | Replaces schedule; also refreshes legacy activeDays / activeHours on the server |
signs | Full replacement array of sign objects |
clientSeatId | Switch client (must exist for your user) |
dealIds, deals, dealId, dealName, publisherId | Deal fields |
Attach creatives by id (use POST /api/partner/v1/creatives above to pre-submit, or create creatives in the web app):
Option A — map each sign targeting key to a list of creative document ids:
bash
curl -sS -X PATCH "$API_HOST/api/partner/v1/campaigns/CAMPAIGN_ID" \
-H "Authorization: Bearer $DSP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"assignedCreatives": {
"SIGN_OR_ADUNIT_KEY": ["creativeDocId1"],
"OTHER_KEY": ["creativeDocId1"]
}
}'Option B — same creatives on every sign the campaign already has:
bash
curl -sS -X PATCH "$API_HOST/api/partner/v1/campaigns/CAMPAIGN_ID" \
-H "Authorization: Bearer $DSP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"applyCreativeIdsToAllSigns": true,
"creativeDocIds": ["creativeDocId1", "creativeDocId2"]
}'Success (200)
json
{
"success": true,
"campaignId": "CAMPAIGN_ID",
"updatedFields": ["name", "totalBudget", "maxCPM", "baseCpmCents", "updatedAt"]
}Turn a campaign on or off (DSP)
POST /api/partner/v1/campaigns/:campaignId/activation
bash
curl -sS -X POST "$API_HOST/api/partner/v1/campaigns/CAMPAIGN_ID/activation" \
-H "Authorization: Bearer $DSP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"activated": true}'Deactivate:
bash
-d '{"activated": false}'Success (200) — example shape:
json
{
"success": true,
"message": "Campaign ... has been activated",
"dspResponse": null,
"dspError": null,
"locallyStored": false,
"pacingInitialized": true,
"pacingData": { },
"biddingConfigured": false,
"biddingConfig": null
}Activation usually requires screens (adunitIds) and creatives to be valid; if activation fails, the JSON may include dspError or a 400 error message.
Play and spend data (PulseFlow Bid Archive)
PulseFlow Bid Archive is the product for billing and play history: the dashboard is at https://archive.vuebillboards.com. The JSON aggregate APIs below (summary, daily, hourly, signs, records) are the same data the dashboard loads; they are served from Pulseflow at PULSEFLOW_HOST, not from API_HOST — see also the Bid Archive HTTP page for routes on archive.vuebillboards.com (for example GET /campaign-info/:campaignId).
Same DSP_API_KEY as the partner API
Use the exact same DSP_API_KEY and headers as the rest of this guide: Authorization: Bearer $DSP_API_KEY or X-Vue-Dsp-Api-Key: $DSP_API_KEY on every request to $PULSEFLOW_HOST/api/archive/billing/....
(Internal only: a signed-in platform-admin browser session on .vuebillboards.com also works without repeating the header.)
bash
export PULSEFLOW_HOST="https://pulseflow.vuebillboards.com"
export DSP_API_KEY="vbdsp.<keyId>.<secret>"Query params are optional unless noted. Dates are typically YYYY-MM-DD. Include one of the auth headers on every curl example below.
Totals for a campaign
bash
curl -sS \
-H "Authorization: Bearer $DSP_API_KEY" \
"$PULSEFLOW_HOST/api/archive/billing/summary?campaignId=CAMPAIGN_ID&startDate=2026-04-01&endDate=2026-04-30"Example success
json
{
"enabled": true,
"totalRecords": 1200,
"totalBilledCents": 450000,
"totalBilledDollars": 4500,
"totalImpressions": 900000,
"avgCpmCents": 500,
"avgCpmDollars": 5,
"uniqueCampaigns": 1,
"uniqueSigns": 12,
"uniqueDsps": 1
}If archive Postgres is off, you may see "enabled": false.
By day
bash
curl -sS \
-H "Authorization: Bearer $DSP_API_KEY" \
"$PULSEFLOW_HOST/api/archive/billing/daily?campaignId=CAMPAIGN_ID&startDate=2026-04-01&endDate=2026-04-30"By hour and screen (playouts / impressions / spend)
bash
curl -sS \
-H "Authorization: Bearer $DSP_API_KEY" \
"$PULSEFLOW_HOST/api/archive/billing/hourly?campaignId=CAMPAIGN_ID&startDate=2026-04-01&endDate=2026-04-07&limit=50000"Row shape (example)
json
{
"enabled": true,
"rows": [
{
"date": "2026-04-01",
"hour": 14,
"signId": "some-sign-id",
"playouts": 42,
"impressions": 42,
"billedCents": 2100,
"billedDollars": 21
}
]
}Per sign (rollup)
bash
curl -sS \
-H "Authorization: Bearer $DSP_API_KEY" \
"$PULSEFLOW_HOST/api/archive/billing/signs?campaignId=CAMPAIGN_ID&startDate=2026-04-01&endDate=2026-04-30"Raw rows (paginated)
bash
curl -sS \
-H "Authorization: Bearer $DSP_API_KEY" \
"$PULSEFLOW_HOST/api/archive/billing/records?campaignId=CAMPAIGN_ID&startDate=2026-04-01&endDate=2026-04-30&page=1&limit=100"Optional filters (most archive GETs)
| Query | Meaning |
|---|---|
dspInstanceId | Limit to one DSP |
campaignId | One campaign |
signId | One screen |
startDate, endDate | Inclusive date range |
Note: Even with a valid key, scope queries with dspInstanceId / campaignId so integrations only pull their own data. If you expose these URLs to external partners, prefer routing through your own gateway and enforcing those filters server-side.
Quick reference
| Goal | Method | Path |
|---|---|---|
| Screens near a point | GET | /api/signs?lat=&lng=&radius= (radius in km) |
| Find one screen | GET | /api/signs/match |
| Find many screens | GET | /api/signs/match/bulk |
| Screens by keys | POST | /api/signs/by-platform-keys |
| Screens by seller | GET | /api/signs/by-seller-id |
| Create campaign | POST | /api/partner/v1/campaigns |
| Pre-submit creative (Hivestack) | POST | /api/partner/v1/creatives |
| Creative status (Hivestack) | GET | /api/partner/v1/creatives/:dspCreativeId?publisherId= |
| Retrieve a campaign | GET | /api/partner/v1/campaigns/:id |
| Edit campaign | PATCH | /api/partner/v1/campaigns/:id |
| Activate / pause | POST | /api/partner/v1/campaigns/:id/activation |
| Bid Archive billing summary | GET | https://pulseflow.vuebillboards.com/api/archive/billing/summary |
| Bid Archive plays by hour | GET | https://pulseflow.vuebillboards.com/api/archive/billing/hourly |
Support
Please email brian@vuebillboards.com with any questions.