Profila Sign up

API Docs

The Profila Brand API — declared signals, creatives, MCP, OpenRTB 2.6.

Brand API

Developer reference

Profila is a consent-aware supply-side platform. Consumers declare purchase intent in the app — a signal — and brands respond with their own inventory over REST, MCP, or GraphQL. You receive declared, privacy-safe targeting only — never names, emails, IPs, device ids, cookies, or precise location.

Base URL https://api.profila.com
Access Verified brands. Request verification at kyb@profila.com.
Auth A brand API key, keyId.secret, minted in the brand portal.
Rate limit 120 requests per minute.
Brand API

Status

What's live today

CapabilityStatus
Mint / revoke API keys (brand portal) Live
Inbound REST — pull signals, push creatives, read logs Live, callable now
Inbound MCP server (/mcp, 4 tools) Live, callable now
GraphQL — billing, invoices, moments, connections Live
Overview counters (endpoints, signals, bid requests, creatives) Live — real counts
Outbound OpenRTB 2.6 — Profila POSTs bid requests to your DSP Not currently sending

The outbound auction does not run, and you cannot switch it on.

Registering and activating a bid endpoint is necessary but not sufficient. The auction is also gated on our side, by a platform setting that ships off. While it is off the match path skips the auction entirely and falls through to Profila’s own content path — so a correctly registered, activated, healthy bid endpoint receives zero bid requests and your outbound counters read 0, with nothing wrong at your end.

An earlier version of this page said the auction starts sending as soon as you register and activate an endpoint. That was wrong. It is corrected here rather than quietly edited, because anyone who built against it was debugging something that was never going to fire.

Talk to us before building the outbound pathkyb@profila.com. The inbound surface below is real and callable today.

Brand API

Concepts

The vocabulary

TermMeaning
Signal A consumer's declared purchase intent, AI-classified into an IAB category + vertical + readiness + preferences. Identified only by an anonymous token (e.g. AU-EU-3841) and an opaque signalUuid — never a person.
Matching A signal reaches you two ways: brand (the consumer named your brand, matched by your domain) or category (the signal’s IAB category matches one your endpoint serves).
Creative A native sponsored card you supply for a matching signal. It renders in that consumer’s feed, tagged with the signal code.
Bid endpoint Your own OpenRTB responder URL. Profila POSTs privacy-first bid requests to it and ingests the winning creative.
API key A keyId.secret pair authenticating the inbound REST + MCP surfaces.
Routing mode brand_specific, marketplace_category, or both — how signals reach an endpoint.
Delivery token Issued when Profila serves your content. Single-use, and the thing that makes a reported event billable.

Inbound — you pull matching signals and push creatives, over REST or MCP. This is the surface to start with; it is live and callable today. GraphQL — your own tooling reads billing, invoices, moments and your connected audience. Also live. Outbound — Profila sends you OpenRTB bid requests, and is not currently sending; see the notice above.

Brand API

Getting access

From verified brand to first call

  • Get verified. Your brand account must be verified. Request it at kyb@profila.com.
  • Open the API tab. Brand portal → Customers → API. The tab is always visible — until your brand is verified it shows a verify to unlock placeholder rather than the key-minting controls.
  • Mint an API key. The secret is shown once — copy keyId.secret immediately. Revoke and re-mint anytime.
  • Optionally register a bid endpoint for the outbound flow.

A key looks like pk_ab12cd34.sk_9f8e7d6c… — the pk_ half is the public key id, the sk_ half is the secret. Profila stores only a hash of the secret; it can be re-minted, never recovered.

Brand API

Authentication

Every inbound request carries the key

Either header works:

Authorization: Bearer pk_ab12cd34.sk_9f8e7d6c…

x-api-key: pk_ab12cd34.sk_9f8e7d6c…
  • Scopes — minted with signals:read + creatives:write by default. Pushing creatives needs creatives:write.
  • Rate limit — 120 requests / minute / brand. Over the limit → 429 rate_limited.
  • Errors401 unauthorized · 403 forbidden · 400 invalid body · 422 validation · 429 rate_limited.
Brand API

Inbound REST

GET /api/brand/openrtb/signals

List anonymous signals that currently match your inventory. Query: limit (1–100, default 50).

curl -s "https://api.profila.com/api/brand/openrtb/signals?limit=20" \
  -H "Authorization: Bearer pk_ab12cd34.sk_9f8e7d6c…"
{
  "signals": [
    {
      "signalUuid": "0f6c8e2a-…",
      "anonymousToken": "AU-EU-3841",
      "vertical": "automotive",
      "readiness": "ready",
      "iabContentCategory": "IAB2-2",
      "iabContentCategoryName": "Auto Type",
      "preferences": ["electric", "SUV"],
      "country": "CH",
      "matchReason": "category",
      "maxPerPeriod": 5,
      "remainingSlots": 3,
      "remainingForYou": 1,
      "createdAt": "2026-06-21T10:00:00.000Z"
    }
  ]
}
FieldNotes
signalUuid Opaque signal id — use it to push a creative.
anonymousToken Human-readable signal code shown to the consumer.
vertical, readiness Declared classification (e.g. automotive, ready).
iabContentCategory(Name) IAB content taxonomy code + label.
preferences Declared preferences, PII-scrubbed.
country Coarse country code only.
matchReason brand (consumer named you) or category (IAB match).
maxPerPeriod How many items the person asked for this period.
remainingSlots What they will still accept from anyone.
remainingForYou What you may still send. Push when this is above zero.

No name, email, phone, IP, device id, or precise location is ever returned.

Brand API

Inbound REST

POST /api/brand/openrtb/creatives

Push a native creative for one of your matching signals. Requires creatives:write.

curl -s -X POST https://api.profila.com/api/brand/openrtb/creatives \
  -H "Authorization: Bearer pk_ab12cd34.sk_9f8e7d6c…" \
  -H "content-type: application/json" \
  -d '{
    "signalUuid": "0f6c8e2a-…",
    "communicationType": "promotion",
    "title": "BMW iX — all-electric SAV",
    "description": "Up to 600 km range. Book a test drive.",
    "imageUrl": "https://www.bmw.com/…/bmw-ix.png",
    "clickUrl": "https://www.bmw.com/en/all-models/bmw-i/ix",
    "cta": "Configure yours",
    "sponsoredBy": "BMW",
    "adomain": ["bmw.com"]
  }'
FieldReqNotes
signalUuid Must be one of your matching signals (else 422).
communicationType What the content is. Selects your rate, so there is no default — see below.
title Card headline.
description Card body.
imageUrl Must be an http(s) URL.
clickUrl Must be an http(s) URL. Click-through destination.
cta Button label (e.g. "Configure yours").
sponsoredBy Sponsor label shown on the card.
adomain Advertiser domain(s).
price, crid Optional bid price / creative id for your records.

Response { "ok": true }, or 422 if the signal doesn't match or the creative isn't renderable. On success the card enters that consumer's feed as a sponsored item.

Pushes made here count as delivered content, but the Creatives served counter in the portal counts auction-won creatives from the outbound flow only. A successful push will not move it — use GET /logs and your own records to track pushes.

Brand API

Inbound REST

Declaring what the content is

communicationType is required and has no default. It is what this content is, and it selects the rate you are charged — so a push without it returns 422 rather than having Profila guess a price on your behalf.

advocacy   brand_building   causes   company_information   product_information   promotion   personalized_offer   special_offer   lead_generation   survey

It is the same set of ten values brands choose from when authoring a moment in the portal, so the API is a different door to the same product rather than a parallel taxonomy.

Declare it accurately. Profila reviews pushed content and may reclassify it before the month closes, which reprices that period’s events. Under-declaring buys nothing — it surfaces on the invoice you were trying to reduce.

Which band each type is priced in, and what happens if we reclassify, is in the Billing Guide.

Brand API

Inbound REST

How much you may send

The person decides, not your budget. Every signal carries how many items its owner asked for in the current period. Your budget bounds what you spend; it authorises nothing on its own.

The signalYour allowance
The person named your brand Their whole remaining allowance for the period
Open to any brand One item per period, and never more than their remaining total

The one-per-period rule on open signals exists so a shared signal is not a race between brands. It is not a Profila quota — whatever brands do not take, Profila’s own results fill, so the person always receives what they asked for.

Every signal in GET /api/brand/openrtb/signals reports this, so you never have to push to find out. remainingSlots is the person’s, remainingForYou is yours, and null in either means they set no ceiling.

A push beyond the limit returns 422 with a code you can match on, not a string:

CodeMeaning
USER_LIMIT_REACHED The person has received everything they asked for this period.
BRAND_LIMIT_REACHED You have used your own allowance for this signal.

Backing off on either is correct behaviour, not an error to retry through. A signal that is paused or has expired is refused too — that is a person who has stopped asking.

Brand API

Inbound REST

GET /api/brand/openrtb/logs

Recent bid requests Profila sent to your endpoints (outbound flow). Query: limit.

Each entry: requestId, dspEndpointId, categoryIds, impCount, responseTimeMs, hadBid, createdAt.

Brand API

MCP server

The same inbound surface, over MCP

Streamable-HTTP JSON-RPC 2.0 for LLM/agent runtimes. Endpoint POST https://api.profila.com/mcp, same key on Authorization or x-api-key, protocol version 2024-11-05.

ToolArgumentsDoes
list_matching_signals { limit?: number } (1–100) Your matching signals.
get_signal_detail { signalUuid } One matching signal's detail.
submit_creative { signalUuid, communicationType, title, description, imageUrl, clickUrl, cta?, … } Push a creative (needs creatives:write).
get_delivery_stats {} { matchingSignals, bidRequests, creativesServed }.
# Handshake
curl -s -X POST https://api.profila.com/mcp -H "content-type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize"}'

# List tools
curl -s -X POST https://api.profila.com/mcp \
  -H "Authorization: Bearer pk_ab12cd34.sk_…" -H "content-type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

# Call a tool
curl -s -X POST https://api.profila.com/mcp \
  -H "Authorization: Bearer pk_ab12cd34.sk_…" -H "content-type: application/json" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call",
       "params":{"name":"list_matching_signals","arguments":{"limit":5}}}'
Brand API

GraphQL

POST /graphql

The surface your own tooling drives: billing and invoices, moments and what they cost, your brand profile, and your connected audience. It is live today.

Endpoint POST https://api.profila.com/graphql
Auth A user access token for a staff member of your brandnot your keyId.secret brand API key.
Introspection Disabled outside local development. Queries are bounded by depth and field count.

Your brand API key authenticates the REST and MCP surfaces only, and it is rejected here. This catches most teams once: they finish the REST quickstart, point the same credential at /graphql, and read the rejection as a broken key. It is not — the two surfaces take different credentials by design.

Because introspection is off, a client that expects to discover the schema at runtime will come up empty. Work from this page and the operations below rather than from a generated client, and ask us for anything you need that is not listed — an omission here is an omission, not a prohibition.

Brand API

GraphQL

Billing and invoices, in eight queries

QueryReturns
brandApiChannel Your API budget, spend, cap state — and which prerequisite is missing.
brandBillingOverview The period summary.
brandRateCardView Your rates, by line. Read prices from here, never hard-coded.
brandInvoices Your invoices.
brandUsage What you used, by day.
brandNotBilled Delivered but not charged, and why.
brandMomentSpend Spend per moment.
brandAudienceTrend Audience size over time.

No pricing numbers appear anywhere in these docs. Rates, country multipliers, size bands and benchmarks are all editable without a deploy, so a number written down here would be a second source of truth that drifts. Read yours from brandRateCardView.

Brand API

GraphQL

Five fields that do not mean what they look like

  • budgetMinor: 0 is a closed channel, not an unlimited one. Read isEnabled rather than inferring the state from the number.
  • isCapped: true means delivery has stopped, not slowed. A capped brand is invisible, not free. The budget is a hard cap, not a spend limit.
  • reclassifiedCount is worth polling. Profila reviews the communicationType you declared and may reclassify before the month closes, which reprices that period. It is surfaced so you find out while you can still ask about it, rather than on the invoice.
  • pricingCategory: null means the moment predates pricing, not that it is free. It is frozen at creation, so moments made before billing existed carry no category.
  • estimate: null means we did not quote you a number — not zero. Expected for a brand with no rate card yet, or a dynamic moment that started with an empty audience.

listConnectionsForBrand returns your audience roster and caps at 1000 rows. Analytics built on it have to be windowed counts rather than totals — a total computed from a capped list is wrong the moment you pass the cap, and silently so.

The person initiates a connection, never the brand. There is no operation that connects a brand to someone who did not choose it. That is the product, not a permission you can be granted.

Brand API

Billing

What you are charged for

Content you push through the API is billed on the same basis as a moment authored in the portal: by what the content is, at the rate on your rate card for that category. The API is a different door to the same product, not a cheaper one.

Two events are billable, exactly as on-platform: an impression and a click-through. Dwell, saves, shares and reactions are recorded and never charged. Impressions are metered end to end and priced at zero.

  • Your API budget is set by Profila, and it is a hard cap. A standing amount across everything you push. When it is reached your content stops being delivered, not merely stops being billed.
  • A brand with no API budget cannot push at all. The push returns an error rather than delivering something that could never be invoiced. With no billing country set, likewise. These are the two most common reasons a new integration delivers nothing.

The commercial explanation — rate cards, price bands, budgets, invoices — is in the Billing Guide.

Brand API

Billing

POST /api/brand/events

Specified, but not yet reachable — do not build against this.

A claim is only billable with a delivery token Profila issued, and nothing currently issues one: there is no serve endpoint yet. Every real POST /api/brand/events therefore returns accepted: false with TOKEN_UNKNOWN, no matter what you send. That is not a misconfiguration at your end, and no amount of debugging will change it.

The contract below is implemented and tested on the receiving side, and it is documented now so the shape is stable when the serving side lands — not so it gets integrated today.

Talk to us before integratingkyb@profila.com. To reconcile what you have actually delivered in the meantime, use GET /api/brand/billing/usage below.

A reported event is a claim, not a fact. Profila is the system of record, so a claim is only billable when it arrives with a delivery token Profila itself issued.

POST /api/brand/events
Authorization: Bearer <keyId>.<secret>

{
  "tokenId": "…",              // issued when Profila serves — see the notice
  "idempotencyKey": "…",       // YOUR id for this event. Required.
  "eventType": "ENGAGEMENT",   // or IMPRESSION
  "engagementKind": "click_through",
  "occurredAt": "2026-04-14T09:30:00Z"
}

idempotencyKey is required and has no default. Retry a timed-out request with the same key and it is recorded once; send a new key and it is a new event. A generated default would make every network retry a double-bill, which is exactly what this endpoint exists to prevent.

The response is 200 whether or not the claim was billable — a rejected claim was still successfully processed — and the reason comes back inline, so you find out within the request rather than at month end.

{ "accepted": false, "rejectionCode": "TOKEN_UNKNOWN", "message": "…" }
Rejection codeMeaning
TOKEN_UNKNOWN No such delivery token. What every real call returns today — see the notice above.
TOKEN_EXPIRED Token past its validity.
TOKEN_REUSED Already redeemed — almost always a retry without an idempotency key.
TOKEN_BRAND_MISMATCH The token was not issued to you.
OUTSIDE_WINDOW Reported time falls outside the token’s window.
PERIOD_FROZEN That month has been invoiced.
TEST_TRAFFIC Recorded as test. Expected on a test brand, and never billed.
DUPLICATE Already reported under this idempotency key. Expected on a retry.
Brand API

Billing

Delivery tokens, and reconciling

  • Tokens are single-use and narrowly bound. One moment, one creative, one person. A token cannot be replayed against a different moment’s budget.
  • Billing happens on redemption, not on issue. Issuing is a delivery attempt, and an ad that never rendered is not something you should pay for.
  • Redemption decides the service period, not your timestamp. Your occurredAt is recorded; it simply does not choose which month the event lands in. Trust the server on timing.
  • Test traffic is derived from your brand record, never from anything you send. Events from a brand marked as test are recorded, never billed, and reported back as TEST_TRAFFIC so you can see the integration working without generating an invoice.
GET /api/brand/billing/usage?period=2026-04-01

Returns what you reported, what was accepted, and what was rejected broken down by reason. This is the endpoint to reconcile against — it is live and callable now, unlike event reporting above. Check it daily rather than discovering a gap at month end: a discrepancy found on the 3rd is a config fix; the same one found at month end is a support ticket about an invoice.

Accepted is not the same as charged. Frequency caps, budget caps and validity checks are applied when the month closes, so your invoice may be lower than your accepted count.

Brand API

Outbound · OpenRTB 2.6

Register a bid endpoint

Profila POSTs an OpenRTB 2.6 bid request to your responder whenever a matching signal appears, then ingests your winning creative. Register in the portal (API tab → Register endpoint):

  • Bid URL — your responder, e.g. https://dsp.yourbrand.com/openrtb/bid
  • Routing — brand + category / brand only / category only
  • Auth — None / Bearer token / API-key header
  • Categories — IAB codes you serve, e.g. IAB2, IAB20

New endpoints start pending — click Activate to receive traffic. First-price (at: 1), tmax default 200 ms, and the request carries x-openrtb-version: 2.6.

Brand API

Outbound · OpenRTB 2.6

The bid request Profila sends

{
  "id": "<request uuid>",
  "at": 1,
  "tmax": 200,
  "imp": [{
    "id": "1",
    "banner": { "format": [{ "w": 300, "h": 250 }] },
    "bidfloor": 0, "bidfloorcur": "USD", "secure": 1,
    "ext": { "profilaBrandDomains": ["bmw.com"] }
  }],
  "site": {
    "id": "profila-web", "name": "Profila", "domain": "app.profila.com",
    "cat": ["IAB2-2"], "publisher": { "id": "profila", "name": "Profila GmbH" }
  },
  "device": { "devicetype": 2, "os": "Unknown", "dnt": 0, "lmt": 1 },
  "user": {
    "id": "<daily-rotating opaque hash>",
    "data": [
      { "name": "profila-declared-categories",
        "segment": [{ "id": "IAB2-2", "name": "IAB2-2" }] },
      { "name": "profila-classified-intent", "segment": [
        { "name": "vertical", "value": "automotive" },
        { "name": "readiness", "value": "ready" },
        { "name": "intent_type", "value": "purchase" },
        { "name": "timeframe", "value": "this_month" },
        { "name": "budget_range", "value": "40000-60000 CHF" }
      ] }
    ]
  },
  "regs": { "coppa": 0, "ext": { "gdpr": 1, "us_privacy": "1YNN" } },
  "source": { "schain": { "ver": "1.0", "complete": 1,
    "nodes": [{ "asi": "profila.com", "sid": "profila-ssp", "hp": 1 }] } }
}
  • bidfloor is always 0Profila does not charge per impression. Impressions are metered end to end and priced at zero: they appear on an invoice with their quantity and an amount of 0.00, so the number is auditable rather than merely asserted. If that ever changes, your rate card will show the price before an invoice does.
  • user.id is a daily-rotating SHA-256 hash — no buyeruid, IP, or device id.
  • lmt: 1 and regs.ext.gdpr: 1 are always set.
Brand API

Outbound · OpenRTB 2.6

The bid response Profila expects

Return a standard BidResponse. The winning creative is read from bid.ext, or from a Native 1.2 adm JSON if you prefer to send one:

{
  "id": "<same request id>",
  "cur": "USD",
  "seatbid": [{
    "seat": "your-seat",
    "bid": [{
      "id": "b1", "impid": "1", "price": 6.5,
      "adomain": ["bmw.com"],
      "crid": "creative-123",
      "ext": {
        "title": "BMW iX — all-electric SAV",
        "description": "Up to 600 km range. Book a test drive.",
        "imageUrl": "https://www.bmw.com/…/bmw-ix.png",
        "clickUrl": "https://www.bmw.com/en/all-models/bmw-i/ix",
        "cta": "Configure yours",
        "sponsoredBy": "BMW"
      }
    }]
  }]
}

Empty seatbid (or HTTP 204) to no-bid. Profila validates the bid against the signal — brand domain for brand_specific, IAB category otherwise — logs it, and renders the winner in the consumer's feed. HTTPS only, no redirects followed on the bid call, capped response size.

Brand API

Reference

Values and counters

Routing modes brand_specific · marketplace_category · both
Endpoint status pendingactive / paused
Endpoint auth types none · bearer_token · api_key
OpenRTB protocol openrtb_2_6 (default) · openrtb_2_5
Key scopes signals:read · creatives:write
CounterMeans
Active endpoints Your registered endpoints with status active.
Matching signals Live count of signals routing to you right now.
Bid requests Bid requests Profila has sent to your endpoints.
Creatives served Auction-won creatives ingested — outbound flow only.
Brand API

Privacy guarantees

What's sent, what's never sent

Sent to brandsNever sent
Anonymous token (rotating) Name, email, phone, address
Declared IAB category + intent segments IP address, device id / IFA
Readiness, budget range, preferences (scrubbed) Cookies, cross-site identifiers
Coarse country, regs.ext.gdpr = 1 Precise location, real user id

Enforced in the bid-request builder and creative-ingest path, and asserted by automated tests. Every consumer sees, in-app, exactly what declared data was sent.