Download OpenAPI specification:
The NORMA Ads API enables programmatic campaign management and reporting for sports bettor push notification inventory.
NORMA delivers ads at the exact moment users have active financial stakes in live games — generating 12–18% CTR on high-intent moments like Bet Resolved, Overtime, and Spread Alert.
This API is designed for use by AI advertising agents and DSP integrations. It supports OAuth 2.0 Client Credentials flow and is MCP-compatible via the norma-ads-mcp server package.
MCP Server: https://mcp.getnorma.app Developer Docs: https://getnorma.app/developers Agent Discovery: https://getnorma.app/adagents.json
Implements RFC 6749 §4.4 Client Credentials grant. Returns a short-lived RS256-signed JWT bearer token. Rate limited to 10 requests/minute per IP.
| grant_type required | string Value: "client_credentials" |
| client_id required | string |
| client_secret required | string |
| scope | string Space-separated list of requested scopes |
grant_type=client_credentials&client_id=norma_client_abc123&client_secret=base64url_secret_here&scope=campaigns%3Aread%20campaigns%3Awrite%20reporting%3Aread
{- "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
- "token_type": "Bearer",
- "expires_in": 3600,
- "scope": "campaigns:read campaigns:write reporting:read"
}Returns all 11 NORMA moment types with floor CPM prices and typical CTR ranges. This endpoint is public (no authentication required) and cached for 1 hour.
[- {
- "key": "bet_resolved",
- "display_name": "Bet Resolved",
- "description": "User's wager is settled",
- "floor_cpm_usd": 0.5,
- "typical_ctr_low": 9,
- "typical_ctr_high": 15,
- "available_sports": [
- "ncaa_basketball",
- "nba",
- "nfl",
- "mlb"
]
}
]Returns floor prices and clearing price percentiles (p25/p50/p75/p90) for all moment types. Data is privacy-safe (no individual bids exposed) and cached for 15 minutes. Useful for bid strategy optimization.
{- "as_of": "2019-08-24T14:15:22Z",
- "moment_types": [
- {
- "moment_type": "bet_resolved",
- "floor_cpm_usd": 0.5,
- "clearing_price_p25_usd": 0.52,
- "clearing_price_p50_usd": 0.65,
- "clearing_price_p75_usd": 0.8,
- "clearing_price_p90_usd": 1.1
}
]
}Returns all campaigns for the authenticated advertiser, newest first.
| status | string Enum: "active" "paused" "ended" "cancelled" Filter by campaign status |
| page | integer >= 1 Default: 1 |
| per_page | integer [ 1 .. 100 ] Default: 20 |
{- "campaigns": [
- {
- "id": "101",
- "name": "DraftKings March Madness",
- "status": "active",
- "moment_types": [
- "string"
], - "sports": [
- "string"
], - "bid_cpm_usd": 0.1,
- "daily_budget_usd": 0.1,
- "total_budget_usd": 0.1,
- "spend_to_date_usd": 0.1,
- "impressions_to_date": 0,
- "start_date": "2019-08-24",
- "end_date": "2019-08-24",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "pagination": {
- "page": 1,
- "per_page": 20,
- "total": 47
}
}Creates a new ad campaign with an initial creative. The campaign is
set to active status immediately. The icon_url must be a publicly
reachable HTTPS URL (verified via HEAD request at creation time).
Bid CPM must meet or exceed the floor price for all selected moment types.
| name required | string |
| moment_types required | Array of strings non-empty Items Enum: "bet_resolved" "close_game" "overtime" "spread_alert" "moneyline_alert" "total_alert" "prop_alert" "position_alert" "foul_trouble" "follow_alert" "prediction_resolved" |
| sports required | Array of strings non-empty Items Enum: "ncaa_basketball" "nba" "nfl" "mlb" |
| bid_cpm_usd required | number <float> CPM bid in USD — must meet or exceed floor for all selected moment types |
| daily_budget_usd required | number <float> >= 5 |
| total_budget_usd required | number <float> >= 10 |
| start_date required | string <date> Campaign start date (must be >= today) |
| end_date | string or null <date> Campaign end date (must be after start_date) |
| target_cpa_usd | number or null <float> Enables CPA auto-bidding via Thompson Sampling |
| postback_url | string or null <uri> |
required | object (CreativeInput) |
{- "name": "DraftKings March Madness",
- "moment_types": [
- "bet_resolved",
- "overtime",
- "close_game"
], - "sports": [
- "ncaa_basketball",
- "nba"
], - "bid_cpm_usd": 0.8,
- "daily_budget_usd": 100,
- "total_budget_usd": 1000,
- "start_date": "2025-03-01",
- "end_date": "2025-04-07",
- "creative": {
- "headline": "DraftKings — Bet $5, Get $150",
- "body": "Your team just went up. Lock in your winnings now.",
- "cta_text": "Bet Now"
}
}{- "id": "101",
- "status": "active",
- "estimated_daily_impressions": 3200,
- "estimated_daily_spend_usd": 2.56,
- "created_at": "2025-03-01T00:00:00Z"
}Returns full campaign detail including the primary creative.
| id required | string Example: 101 |
{- "id": "101",
- "name": "DraftKings March Madness",
- "status": "active",
- "moment_types": [
- "bet_resolved",
- "overtime",
- "close_game"
], - "sports": [
- "ncaa_basketball",
- "nba"
], - "bid_cpm_usd": 0.8,
- "daily_budget_usd": 100,
- "total_budget_usd": 1000,
- "target_cpa_usd": 25,
- "start_date": "2025-03-01",
- "end_date": "2025-04-01",
- "spend_to_date_usd": 42.5,
- "impressions_to_date": 8500,
- "creative": {
- "id": "42",
- "headline": "DraftKings – Bet $5, Get $150",
- "body": "Your team just went up. Lock in your winnings now.",
- "cta_text": "Bet Now",
- "status": "pending",
- "performance_score": 0.73,
- "variant_label": "variant_a"
}, - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Updates mutable campaign fields. The following fields are immutable after
creation and cannot be changed: moment_types, sports, start_date,
creative. Bid changes propagate to the bids table synchronously.
| id required | string Example: 101 |
| name | string |
| bid_cpm_usd | number <float> |
| daily_budget_usd | number <float> |
| total_budget_usd | number <float> |
| target_cpa_usd | number or null <float> |
| end_date | string or null <date> |
| status | string Enum: "active" "paused" "ended" |
| postback_url | string or null <uri> |
{- "bid_cpm_usd": 0.95,
- "daily_budget_usd": 150
}{- "id": "101",
- "name": "DraftKings March Madness",
- "status": "active",
- "moment_types": [
- "bet_resolved",
- "overtime",
- "close_game"
], - "sports": [
- "ncaa_basketball",
- "nba"
], - "bid_cpm_usd": 0.8,
- "daily_budget_usd": 100,
- "total_budget_usd": 1000,
- "target_cpa_usd": 25,
- "start_date": "2025-03-01",
- "end_date": "2025-04-01",
- "spend_to_date_usd": 42.5,
- "impressions_to_date": 8500,
- "creative": {
- "id": "42",
- "headline": "DraftKings – Bet $5, Get $150",
- "body": "Your team just went up. Lock in your winnings now.",
- "cta_text": "Bet Now",
- "status": "pending",
- "performance_score": 0.73,
- "variant_label": "variant_a"
}, - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Adds a new creative variant to an existing campaign for A/B testing. Traffic is split equally across all approved variants. Variants are labeled variant_a, variant_b, variant_c, etc. automatically.
| id required | string |
| headline required | string <= 60 characters |
| body required | string <= 120 characters |
| icon_url required | string <uri> Must be a publicly reachable HTTPS URL |
| action_url required | string <uri> |
| cta_text | string or null <= 20 characters |
{- "headline": "DraftKings — Free $150 Bet",
- "body": "The game is getting good. Don't miss your chance.",
- "cta_text": "Claim Now"
}{- "id": "43",
- "variant_label": "variant_b",
- "traffic_allocation": 0.5
}Returns floor prices and clearing price percentiles (p25/p50/p75/p90) for all moment types. Data is privacy-safe (no individual bids exposed) and cached for 15 minutes. Useful for bid strategy optimization.
{- "as_of": "2019-08-24T14:15:22Z",
- "moment_types": [
- {
- "moment_type": "bet_resolved",
- "floor_cpm_usd": 0.5,
- "clearing_price_p25_usd": 0.52,
- "clearing_price_p50_usd": 0.65,
- "clearing_price_p75_usd": 0.8,
- "clearing_price_p90_usd": 1.1
}
]
}Returns aggregated performance metrics for a campaign over a date range. Optional breakdown by moment_type or creative for granular analysis. Rate limited to 60 requests/minute per advertiser.
| id required | string |
| start_date required | string <date> Example: start_date=2025-03-01 |
| end_date required | string <date> Example: end_date=2025-03-31 |
| breakdown | string Enum: "day" "moment_type" "sport" "creative" "hour_of_day" Example: breakdown=moment_type |
| timezone | string Default: "UTC" Example: timezone=America/New_York |
{- "campaign_id": "101",
- "period": {
- "start": "2025-03-01",
- "end": "2025-03-31",
- "timezone": "UTC"
}, - "totals": {
- "impressions": 8500,
- "clicks": 1020,
- "ctr": 0.12,
- "conversions": 34,
- "spend_usd": 629,
- "average_cpm_paid_usd": 0.74,
- "as_of": "2025-04-01T00:00:00Z"
}
}Returns impression, click, and spend data over time. Use granularity=day
for daily aggregates (reads from materialized view, fast) or
granularity=hour for hourly data (reads raw impressions, slower).
| id required | string |
| start_date required | string <date> |
| end_date required | string <date> |
| granularity | string Default: "day" Enum: "hour" "day" |
{- "campaign_id": "101",
- "granularity": "hour",
- "series": [
- {
- "ts": "2025-03-01T00:00:00Z",
- "impressions": 420,
- "clicks": 50,
- "spend_usd": 31.2
}
]
}Returns impression, click, and conversion metrics for a specific creative.
| id required | string Example: 42 |
{- "creative_id": "42",
- "impressions": 2100,
- "clicks": 315,
- "ctr": 0.15,
- "conversions": 12,
- "spend_usd": 157.5,
- "traffic_allocation": 0.5
}{- "endpoints": [
- {
- "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
- "events": [
- "impression.served"
], - "is_active": true,
- "batch_impressions": true,
- "failure_count": 0,
- "last_delivered_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z"
}
]
}Registers a URL to receive NORMA event notifications. A 32-byte hex
signing secret is generated and returned once — store it immediately.
Use the X-Norma-Signature header (sha256=<hex>) to verify delivery.
NORMA signs with HMAC-SHA256 using the shared secret:
X-Norma-Signature: sha256=<hmac_hex>
| url required | string <uri> |
| events required | Array of strings (WebhookEvent) non-empty Items Enum: "impression.served" "click.recorded" "conversion.recorded" "campaign.budget_50pct" "campaign.budget_90pct" "campaign.ended" "campaign.bid_adjusted" |
| batch_impressions | boolean Default: false Batch impression.served events instead of sending one per impression |
{- "events": [
- "conversion.recorded",
- "campaign.ended"
], - "batch_impressions": false
}{- "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
- "events": [
- "impression.served"
], - "is_active": true,
- "batch_impressions": true,
- "failure_count": 0,
- "last_delivered_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "secret": "a3f9b2c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1",
- "warning": "Save this secret — it will not be shown again. Use it to verify X-Norma-Signature headers."
}Sets the endpoint to inactive. Delivery will stop. Cannot be re-activated via API.
| id required | string <uuid> |
{- "deactivated": true,
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08"
}Fires a conversion.recorded test event to the endpoint. Useful for
verifying connectivity and signature verification before going live.
| id required | string <uuid> |
{- "success": true,
- "status_code": 200,
- "duration_ms": 87,
- "message": "Test event delivered successfully"
}Records a conversion event for a click that originated from a NORMA ad. The GET method is standard for mobile attribution partners who use URL macros in postback URLs.
Deduplication is enforced: if converted=true is already set on the
click, returns { status: "already_recorded" }. Use idempotency_key
to safely retry without creating duplicates.
Conversions outside the 7-day attribution window are rejected.
| campaign_id required | string Example: campaign_id=101 |
| click_id required | string Example: click_id=clk_abc123def456 |
| event_type required | string Enum: "install" "registration" "deposit" "purchase" "custom" Example: event_type=deposit |
| event_value_usd | number <float> Example: event_value_usd=100 |
| event_name | string Example: event_name=first_bet |
| idempotency_key | string Example: idempotency_key=dep_xyz789 |
{- "status": "recorded"
}Records a conversion event via JSON body. Equivalent to the GET endpoint but accepts a JSON body instead of query parameters. Both formats are supported for maximum partner compatibility.
| campaign_id required | string Campaign ID the click belongs to |
| click_id required | string NORMA-issued click identifier from the ad deep link |
| event_type required | string Enum: "install" "registration" "deposit" "purchase" "custom" |
| event_value_usd | number or null <float> Monetary value of the conversion event |
| event_name | string or null Required when event_type is custom |
| idempotency_key | string or null Prevents duplicate recording of the same conversion |
{- "campaign_id": "101",
- "click_id": "clk_abc123def456",
- "event_type": "deposit",
- "event_value_usd": 100,
- "idempotency_key": "dep_xyz789"
}{- "status": "recorded"
}