Ads Placement API Reference

Everything below is the live production contract. One endpoint, five body-selected actions.

On this page:
Base URL Onboarding Authentication & signing Actions Placement object Targeting reference Delivery & selection Billing Limits Error codes

Base URL

POST https://api.blasts.app/api/ping/ads/placement

HTTPS only. POST only — other methods return 405. All requests and responses are application/json.

Onboarding

Partner accounts are provisioned manually — email AL@SavingsSites.com. You receive, exactly once:

CredentialUsed for
api_keyThe Authorization: Bearer header. Identifies your partner account.
signing_secretHMAC-SHA256 request signing (below). Never sent on the wire.

Your account is configured with three negotiated settings: ads enabled (the gate for this API), a max payout weight ceiling, and a monthly send cap that bounds your post-paid exposure. Sandbox credentials are available on request for integration testing.

Authentication & request signing

Every request carries three headers:

HeaderValue
AuthorizationBearer <api_key>
X-Blasts-TimestampUnix time in milliseconds. Rejected if more than 5 minutes off server time (timestamp_expired).
X-Blasts-Signaturesha256=<hex> — HMAC-SHA256 of the signing payload (below) keyed with your signing_secret.

The signing payload is the timestamp and the exact raw request body joined by a single newline character:

payload   = timestamp + "\n" + rawBody
signature = "sha256=" + hex( HMAC_SHA256( signing_secret, payload ) )
Sign the bytes you send.

Serialize the JSON once, sign that exact string, and send that exact string as the body. Re-serializing after signing (different key order, whitespace) makes the signature invalid. The separator is a newline (\n), not a dot.

Optionally send Idempotency-Key (up to 128 characters) on create requests. Replays with the same key return the original deal_id with "duplicate": true instead of creating a second placement.

See Code Examples for working signing code in Node, Python, and cURL.

Actions

The JSON body always includes an action field. Successful responses are 200 with "ok": true; failures return "ok": false plus an error code (see Error codes).

create Add a placement

{
  "action": "create",
  "deal": { … placement object, see below … }
}

Response:

{ "ok": true, "deal_id": "p_yourpartnerid_summer_promo", "ad": { …stored row… } }

The server namespaces your deal_id: whatever suffix you supply (lowercased, a-z 0-9 _ -, max 48 chars) is stored as p_<partner_id>_<suffix>. If you omit it, a timestamp-based suffix is generated. Use the returned deal_id in all later calls. Duplicate ids return deal_id_exists.

update Modify a placement

{
  "action": "update",
  "deal_id": "p_yourpartnerid_summer_promo",
  "deal": { "payout_weight": 80, "creative": { "copy": "New copy…" } }
}

A partial patch: supplied fields are merged onto the stored row (creative merges field-by-field; targeting replaces wholesale), then the merged object is re-validated end to end. Response mirrors create.

delete Retract a placement

{ "action": "delete", "deal_id": "p_yourpartnerid_summer_promo" }

Response: { "ok": true, "deal_id": "…", "deleted": true }. The placement leaves rotation within about 60 seconds (server catalog cache TTL).

list Live placements + caps snapshot

{ "action": "list" }
{
  "ok": true,
  "partner_id": "yourpartnerid",
  "count": 12,
  "max_ads": 100,
  "monthly_send_cap": 250000,
  "month_to_date_sends": 48210,
  "ads": [ { …placement rows… } ]
}

ledger Per-placement performance (billing source)

{ "action": "ledger" }
{
  "ok": true,
  "partner_id": "yourpartnerid",
  "month": "2026-07",
  "month_to_date_sends": 48210,
  "monthly_send_cap": 250000,
  "deals": [
    {
      "deal_id": "p_yourpartnerid_summer_promo",
      "expires_at": 1722470400000,
      "totals":        { "sends": 9120, "views": 6011, "clicks": 402 },
      "last_7_days":   { "sends": 2210, "views": 1544, "clicks": 118 },
      "month_to_date": { "sends": 9120, "views": 6011, "clicks": 402 }
    }
  ]
}

Sends are metered server-side at append time and are authoritative. Views and clicks are reported best-effort by recipient clients — treat them as engagement signal, not an invoice line.

Placement object

{
  "deal_id": "summer_promo",
  "expires_at": 1722470400000,
  "payout_weight": 50,
  "price_usd": 9.99,
  "main_category": "home-services",
  "sub_category": "security",
  "creative": {
    "copy": "20% off first-year monitoring for new movers.",
    "url": "https://advertiser.example.com/offer",
    "audio_url": "https://cdn.example.com/spot.webm",
    "audio_title": "New mover special — 30 second spot",
    "audio_duration_ms": 30000
  },
  "snap_badge": { "shape": "pill", "color": "#1E1E1E", "label": "AD" },
  "targeting": { … see targeting reference … }
}
FieldRequiredRules
expires_atYesUnix milliseconds, integer, in the future, at most 30 days out. Expired placements drop from rotation automatically — refresh long-running campaigns by updating expires_at.
creative.copyYes*Plain text, max 500 characters. Shown as the ad body.
creative.urlYes*HTTPS only, max 2048 characters. The click-through link.
creative.audio_urlNo*HTTPS only. *An audio-only placement (no copy/url) is valid: the audio URL doubles as the link and the title becomes the copy.
creative.audio_titleNoMax 120 characters.
creative.audio_duration_msNo1 to 600000 (10 minutes).
payout_weightNoInteger ≥ 0; default 0. Server-clamped to your account's max payout weight — sending a bigger number silently stores the ceiling.
deal_idNoSuffix only; server prefixes p_<partner_id>_. Lowercase a-z 0-9 _ -, max 48 chars.
price_usdNo0.01 – 9999.99 (display price for the offer).
main_category / sub_categoryNoFree-form slugs (max 64) describing the advertiser. Used to keep multi-ad Blasts category-distinct.
snap_badgeNo{ shape, color, label } — label max 12 chars, defaults to "AD". The chip shown with the ad.
targetingNoOmit for run-of-network. See below.

All creative text passes a family-safe content filter (copy, audio_title, badge label). Rejected content returns content_not_allowed plus the offending field.

Targeting reference

"targeting": {
  "target_mode": "both",
  "zips": ["07001", "07002"],
  "geo_radius": [ { "zip": "07104", "miles": 25, "branch": "both" } ],
  "resident_filters": {
    "genders": ["female"],
    "age_groups": ["under_36", "36_60"]
  },
  "business_filters": {
    "org_sizes": ["solo", "2_10"],
    "primary_roles": ["founder_owner"],
    "target_markets": ["b2b"],
    "business_categories": ["real_estate", "home_services"]
  }
}
FieldAccepted values
target_moderesident · business · both (default both)
zipsUp to 5,000 five-digit US ZIP codes. Empty/omitted = no geo restriction.
geo_radiusUp to 12 entries of { zip, miles, branch }. Miles 0.25–100. branch: resident · business · both. A single object (not array) is also accepted.
resident_filters.gendersmale · female
resident_filters.age_groupsunder_36 · 36_60 · 61_plus
business_filters.org_sizessolo · 2_10 · 11_50 · 50_plus
business_filters.primary_rolesfounder_owner · sales_marketing · operations · eng_it
business_filters.target_marketsb2b · b2c · both
business_filters.business_categoriesUp to 24 slugs from the list below.

Business category slugs: software_technology, it_services_cybersecurity, marketing_advertising, media_creative_design, legal_compliance, accounting_tax, consulting_business_services, architecture_engineering, real_estate, construction_trades, manufacturing, logistics_supply_chain, agriculture_natural_resources, energy_utilities, automotive, home_services, retail_ecommerce, food_beverage, hospitality_travel, fitness_recreation, finance_insurance, healthcare_medical, pharma_biotech, education_training, nonprofit, government_public_sector, associations_membership, telecommunications, other.

Filters combine with AND: a recipient must match the branch, the geo set (explicit ZIPs ∪ radius ZIPs), and every demographic filter you set. Unset filters match everyone. Targeting is evaluated entirely on our servers against the recipient's self-declared profile — no recipient data is ever returned to you.

Delivery & selection

Billing

Limits

LimitValue
Live placements per partner100
Max placement lifetime30 days (then refresh via update)
API rate limit100 requests/minute (default; negotiable). Exceeding returns 429 with retry_after_seconds.
Timestamp skew±5 minutes
Creative copy500 characters
URLsHTTPS only, 2048 characters
Audio duration10 minutes

Error codes

HTTPerrorMeaning
401missing_api_key / invalid_api_keyBearer header absent or key not recognized.
401invalid_signature / missing_signatureHMAC doesn't match the raw body + timestamp. Check the newline separator and that you signed the exact bytes sent.
401timestamp_expired / invalid_timestampX-Blasts-Timestamp missing, non-numeric, or more than 5 minutes off.
403partner_inactiveAccount suspended — contact us.
403ads_not_enabledYour partner account isn't provisioned for the Ads API.
429rate_limit_exceededOver your requests/minute. Retry after retry_after_seconds.
409ads_cap_exceededAlready at 100 live placements. Delete or let some expire.
404deal_not_founddeal_id doesn't exist under your account (or already expired and was pruned).
400expires_at_required / expires_at_in_past / expires_at_too_farExpiry missing, not a future ms-epoch integer, or beyond 30 days.
400invalid_dealCreative incomplete — needs copy + HTTPS url, or an HTTPS audio_url.
400content_not_allowedCreative failed the content filter; response includes the field.
400deal_id_existscreate with a suffix you already used. Use update or a new suffix.
400invalid_action / malformed_json / deal_id_required / deal_object_requiredRequest shape problems.
Questions or credentials: AL@SavingsSites.com · Working code: Node / Python / cURL examples