Documentation

Connect Alterion Labs to your AI tools over MCP, or call the REST APIs directly. One brand key, every marketing agent.

Introduction

Alterion Labs gives your AI tools and your code direct access to every marketing agent. Connect over the Model Context Protocol (MCP) to let your AI coding tool apply marketing-grade changes, or call the REST APIs to read your brand and affiliate data, publish content, and run the Content Agent from your own stack. This reference covers both.

All REST requests are made to:

http
https://alterionlabs.com/v1

Authentication

The REST API uses OAuth 2.0 with the client_credentials grant, the standard machine-to-machine flow. Create an API client on the API & MCP access page: you get a client_id and a client_secret (shown once). Keep the secret server-side.

Discovery is fully self-describing. Protected resource metadata (RFC 9728) names the authorization server that issues tokens for this API; authorization server metadata (RFC 8414) gives the token URL, JWKS, and scopes:

http
https://alterionlabs.com/.well-known/oauth-protected-resource
https://alterionlabs.com/.well-known/oauth-authorization-server

Exchange your credentials for a short-lived access token, then send it as a bearer token on every request. The token endpoint is the token_endpoint from the metadata above.

bash
# 1) Get an access token
curl -s -X POST "$TOKEN_ENDPOINT" \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d "grant_type=client_credentials" \
  -d "scope=https://api.alterionlabs.com/brand.read https://api.alterionlabs.com/affiliate.read"

# Response: { "access_token": "...", "token_type": "Bearer", "expires_in": 3600 }

# 2) Call the API with the token
curl https://alterionlabs.com/v1/brand \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Access tokens expire after 60 minutes. Request a new one when it expires. A missing or invalid token returns 401; a valid token without the required scope returns 403. Both carry a WWW-Authenticate header.

The MCP server authenticates separately, with a connection token you create on the same page (see Connect), not an OAuth client.

Errors

Alterion Labs uses conventional HTTP status codes. Codes in the 2xx range indicate success, 4xx indicate a problem with the request, and 5xx indicate a server error. Every error returns a JSON body:

json
{
  "error": {
    "type": "invalid_request",
    "code": "missing_field",
    "message": "Field 'title' is required."
  }
}
400invalid_request

The request was malformed or missing a required field.

401authentication_error

No valid API key was provided.

403permission_error

The key is valid but lacks access to this resource.

404not_found

The requested resource does not exist.

429rate_limit_exceeded

Too many requests. Back off and retry.

500api_error

Something went wrong on our end.

Rate limits

Requests are rate limited per brand. Current limits and remaining quota are returned on every response:

http
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 598
X-RateLimit-Reset: 1717977600

Exceeding the limit returns 429 rate_limit_exceeded. Back off until the Unix timestamp in X-RateLimit-Reset, then retry.

MCP Server

Connect your AI tool to every agent

The MCP server exposes each marketing agent as a callable tool. Your AI coding tool calls them; Alterion Labs returns structured, ready-to-apply changes, so you ship marketing-grade code without writing it.

Connect

Add Alterion Labs to any MCP-compatible tool (Cursor, Claude Code, Windsurf, VS Code + Copilot, JetBrains AI) with one config block. Create a connection token on the API & MCP access page: it is shown once, inside this block, ready to paste. On connection, the server authenticates your token and exposes the tools below.

mcp-config.json
{
  "mcpServers": {
    "alterion-labs": {
      "url": "https://mcp.alterionlabs.com",
      "headers": { "Authorization": "Bearer at_your_connection_token" }
    }
  }
}

Tools

Each tool maps to an agent capability. Inputs and outputs are JSON.

audit_marketingtool

Run a full marketing audit for a URL. Returns scored findings across SEO, content, GEO, analytics, and retention.

get_seo_fixestool

Return prioritized, ready-to-apply SEO fixes: meta tags, schema, headings, internal links.

get_content_plantool

Return a content calendar and article briefs based on keyword gaps.

get_geo_recommendationstool

Return structure and schema changes that surface your content in AI search engines.

get_tracking_setuptool

Return analytics and conversion tracking scripts to install.

get_retention_actionstool

Return retention logic and cancel-flow changes to reduce churn.

Example call to get_seo_fixes:

json
// Input
{ "url": "https://yourapp.com" }

// Output
{
  "fixes": [
    {
      "id": "meta-description-missing",
      "priority": "high",
      "page": "/pricing",
      "change": "Add a 155-character meta description.",
      "snippet": "<meta name=\"description\" content=\"...\">"
    }
  ]
}

Affiliate tracking install

Install affiliate tracking on your site

One script, two conversion calls, and up to three signed server endpoints. This chapter is the whole contract between your site and Alterion Labs: what the script sends, how a sale is reported, how a signup keeps a late-converting trial attributing, how a refund reverses a commission, how a cancellation stops one from renewing, and what to put in your own privacy notice.

Head script

Paste one tag into your production site's head. It loads async and never blocks the page.

html
<script async src="https://alterionlabs.com/track.js" data-alterion="pk_..."></script>

Copy the tag from your install panel rather than typing it. The script reads its own src to work out where to send everything else, so every browser-side call it makes, the beacons, the conversion pixel, the signup call, moves with this one tag and needs no origin of its own in your code. Your server-side calls are different: the postback URL your install panel gives you, and the refund and cancellation endpoints printed below, carry this same origin baked in, and they do not move on their own when your tracking domain does.

Serve every referral link, the head script and the conversion pixel from a domain you own, once Alterion Labs has enabled it for your account. Blocklists that take out third-party tracking leave a first-party domain alone. Point a subdomain you control at us with a CNAME record, then verify it. Verifying confirms DNS only: your links keep working from the platform origin until Alterion Labs provisions the certificate and the CDN alias for your domain and turns serving on. Once it moves, two of your tracking cookies change which domain they are set on, and so does the origin baked into the postback URL your install panel gives you, and the refund and cancellation endpoints printed below: update those server-side calls, or they keep firing at the platform origin. Privacy guidance covers what the cookie change means for your notice.

Every payload the script posts carries v: 2, the version of this contract. A payload with no version reads as version 2. Nothing else is accepted.

What the script sends

The script reports a referred visit once on arrival. It then reports how long the page was visible: once when the page has been visible for ten seconds, and again on tab hide or page close, skipping a repeat send whose total has not grown since the last one. The beacon sends the page path, never the query string.

POST/api/affiliate/track/beacon

The arrival beacon, sent once when the browser reaches your page on a direct affiliate link, carries:

json
{
  "v": 2,
  "kind": "arrival",
  "key": "pk_0123456789abcdef01234567",
  "clickId": "6f1e3c9a-24b7-4a1d-9f0e-5c8b2a7d4e13",
  "path": "/pricing",
  "client": {
    "languages": "en-US,en",
    "timezoneOffsetMinutes": -60,
    "screenWidth": 1512,
    "screenHeight": 982,
    "hardwareConcurrency": 8
  }
}
vinteger

Contract version. Absent reads as 2.

kindstring

Beacon type: arrival or engagement.

keystring

Public tracking key from the head script tag.

clickIduuid

Click identifier this report updates.

pathstring

Page path, not stored in version 2.

clientobject

Coarse device and browser blob, hashed on the server.

token, campaignId and cookieId are accepted on the arrival beacon and not stored in version 2.

On a redirect link, the one your share links use, the click is already recorded server-side before the browser runs, so unless the script posts a visit report (below), the arrival beacon is the browser's only chance to report prior activity. Three more fields ride it there:

json
{
  "v": 2,
  "kind": "arrival",
  "key": "pk_0123456789abcdef01234567",
  "clickId": "6f1e3c9a-24b7-4a1d-9f0e-5c8b2a7d4e13",
  "path": "/pricing",
  "client": {
    "languages": "en-US,en",
    "timezoneOffsetMinutes": -60,
    "screenWidth": 1512,
    "screenHeight": 982,
    "hardwareConcurrency": 8
  },
  "seenFirstAt": "2026-02-02T18:41:00Z",
  "seenSessions": 3,
  "storageOk": true
}
seenFirstAtstring

When alterion_seen says this visitor was first seen on your site. Optional; absent is not a zero.

seenSessionsinteger

How many separate visits alterion_seen counted before this one. Optional; absent is not a zero.

storageOkboolean

Whether the browser could read its own cookies. Optional; see Prior activity below for what its absence means.

The engagement beacon, sent once the page has been visible for ten seconds and again on tab hide or page close when the total has grown, carries:

json
{
  "v": 2,
  "kind": "engagement",
  "key": "pk_0123456789abcdef01234567",
  "clickId": "6f1e3c9a-24b7-4a1d-9f0e-5c8b2a7d4e13",
  "path": "/pricing",
  "visibleMs": 41200
}
vinteger

Contract version. Absent reads as 2.

kindstring

Beacon type: arrival or engagement.

keystring

Public tracking key from the head script tag.

clickIduuid

Click identifier this report updates.

pathstring

Page path, not stored in version 2.

visibleMsinteger

Milliseconds the referred page was visible, capped at 60,000.

A send is accepted only when its click is no more than thirty minutes old: an engagement beacon for an older click is silently dropped, so a fresh organic session on a device carrying a days-old referral cookie can never overwrite that old click's own measurement. What is kept is the maximum of every accepted send for that click, never the last one, so a visitor who tabs away at two seconds and comes back to read for ten minutes still gets credit for the cap above, sixty seconds, not the two. A referred visit that never sends an engagement beacon at all leaves this column empty, not zero: empty means unmeasured, zero means measured and instant, and fraud review treats the two differently.

Prior activity

The script keeps a second cookie, alterion_seen, recording that a visitor was on your site before they arrived through an affiliate link. It holds three values: when they were first seen, when they were last seen, and how many separate visits that adds up to. It is written only on a visit that carries no referral link, and writing it sends no request: a visitor who never clicks an affiliate link produces no record of themselves on our side. The script does still fetch its configuration from Alterion Labs on every page load, referred or not, and that request only confirms your installation is live. Visits are counted by a thirty-minute gap, so browsing several pages in one sitting counts once.

When a referred visitor arrives on a direct link, the script always reports what that cookie held, alongside the referral it was already reporting. On a redirect link it reports only when storage is confirmed working, and says nothing at all when storage is blocked, because that click is already recorded server-side with nothing left to add. We record the answer on the click: prior activity, no prior activity, or unknown. Unknown is what we record when the browser blocks cookies, when the cookie cannot be read, or when it claims a first-seen time in the future, because none of those is the same as a visitor who had never been to your site, and reporting them as no prior activity would quietly overstate how cold your referrals were. On a redirect link where nothing was reported, the column is left null rather than stamped unknown, which reads the same way to anyone querying it.

seenFirstAt, seenSessions and storageOk ride on the visit report whenever the script sends one, and otherwise on the arrival beacon for a redirect link, because that click is recorded before the browser runs. All three are optional and an absent field is not a zero. Once a beacon request passes rate limiting and validation, it answers ok whatever happens, it never blocks the page, and a second report for the same click is accepted and ignored. The visit endpoint below is less forgiving. It fires on a direct link, and on a redirect link only when first-touch is on and the visitor already holds a referral cookie, so we can decide whether that earlier referral still holds. That visit resolves to the click the redirect already recorded. An unrecognised key answers {"ok": false}, and a database write failure answers a 500.

POST/api/affiliate/track/visit
json
{
  "v": 2,
  "key": "pk_0123456789abcdef01234567",
  "token": "9f2c1a4e7b3d",
  "cookieId": "66b77216-c14b-48b9-b980-695a08e97e02",
  "heldToken": "4b8e0d2c6a1f",
  "framed": false,
  "landingUrl": "https://your-site.com/pricing?via=9f2c1a4e7b3d",
  "referrer": "https://a-partner-review.com/best-tools",
  "seenFirstAt": "2026-02-02T18:41:00Z",
  "seenSessions": 3,
  "storageOk": true
}
keystring

Public tracking key from the head script tag.

tokenstring

The referral token read from the URL.

cookieIdstring

Optional. The device cookie id the script already holds, when it holds one.

heldTokenstring

Optional. The referral token the script's cookie already holds. Under first-touch, that referral keeps the visitor only while its partner is active in your program.

framedboolean

Optional. Whether the landing page runs inside a frame from another origin. A frame from your own origin does not count. The script sets it.

landingUrlstring

Optional. The full address of the page the visitor landed on, including its query string.

referrerstring

Optional. The address of the page that referred them, when the browser sends one.

seenFirstAt / seenSessions / storageOkmixed

The prior-activity report from alterion_seen, described above. All three optional; absent is not a zero.

Conversion paths

We record sales from every path you wire. The primary conversion path is recorded on the campaign and shown on the install panel, and it does not yet decide which record we keep when two paths report the same sale.

JavaScript

Call this on your success page. We attribute it from the referral cookie, so the page never sees affiliate identity.

javascript
alterion.convert({
    orderId: "ORDER_ID",       // your unique order/invoice id
    amountCents: 4900,         // integer cents
    currency: "USD",
    customerId: "CUSTOMER_ID", // enables recurring attribution
    // newCustomer: true       // send true when this buyer has never paid you before
});

alterion.convert() takes your order id, the amount in the currency's smallest unit (cents for USD and EUR, yen for JPY), the currency and your customer id, for a sale. Skip the order id on a page carrying a Stripe checkout session id: the script reads it from the URL and uses that instead. Skip the customer id and the sale still records, but as an anonymous conversion: it can never be correlated with any other sale, so it earns exactly one commission, is never capped, is never halted by a cancellation, and can never renew, even on a recurring campaign. Send it whenever you can. A signup, which carries no order, is reported with alterion.signup() below instead. The script adds the rest of the payload itself.

json
{
  "v": 2,
  "key": "pk_0123456789abcdef01234567",
  "orderId": "cs_test_a1b2c3",
  "merchantOrderId": "ORD-1041",
  "amountCents": 4900,
  "currency": "USD",
  "type": "sale",
  "customerId": "cus_QX1a2b3c",
  "token": "9f2c1a4e7b3d",
  "cookieId": "66b77216-c14b-48b9-b980-695a08e97e02",
  "client": {
    "languages": "en-US,en",
    "timezoneOffsetMinutes": -60,
    "screenWidth": 1512,
    "screenHeight": 982,
    "hardwareConcurrency": 8
  },
  "test": false,
  "newCustomer": true
}

This is what leaves the browser when the call declares newCustomer. orderId is the Stripe checkout session id when one is on the page and your own order id otherwise, merchantOrderId carries your order id alongside it, token and cookieId come from the referral cookie, and client is the coarse device signature described under Privacy guidance. A merchant posting to this endpoint directly, rather than through alterion.convert(), may also send code (a redeemed referral code, handled the way the postback describes it below) and customerEmail.

Signups

Call this from your post-signup page. It gives a trial that converts later a marker to attribute through once your referral cookie is gone, on top of whatever the cookie itself already covers. It records no amount and mints no commission on its own.

javascript
alterion.signup({
    customerId: "CUSTOMER_ID",       // enables the later paid event to inherit this signup
    customerEmail: "CUSTOMER_EMAIL", // used when customerId is not available yet
    // newCustomer: true             // send true when this buyer has never paid you before
});

alterion.signup() takes your customer id, your customer's email, or both, at least one of the two. It sends nothing when neither is set, and nothing when the referral cookie is gone: there is nothing to attribute through.

The paid event inherits the affiliate through customer identity, not through anything the paid call sends about the referral itself: send the same customer id or the same email on the paid call that you sent here. That identity is the join. A paid event carrying no matching identifier finds no signup to join, records as organic, and pays no commission, with no error anywhere to say why.

customerIdstring

Your customer id. Send the same id on the paid call, or the sale has nothing to match.

customerEmailstring

The buyer's email, used when customerId is not available yet. Send the same email on the paid call, or the sale has nothing to match.

currencystring

Three-letter currency code. Optional and unused: a signup records no amount, so nothing is computed from it.

testboolean

Send true to mark a test call, the same as test on alterion.convert().

newCustomerboolean

Commented out on purpose. See New and existing customers below.

The signup-to-paid window

A signup's own reach forward is bounded, not unlimited, and by default it is no longer a reach than your referral cookie already gives you: it lasts your campaign's cookie window, counted from the day of signup rather than the day of the original click. A trial that runs longer than your cookie window is not covered by that default alone.

Set your campaign's trial length to your product's real trial, and the window becomes the trial length plus a fourteen-day grace period, so a card that fails on the first attempt and succeeds on the third still attributes. Turn on your campaign's unlimited-window option instead for a freemium product with no trial and no natural bound; nothing falls into that state by accident, you have to choose it. Whichever window applies, the paid event still needs the matching customer id or email described above: the window decides how long that join stays open, not whether one exists.

Stripe Checkout

This path needs a connected account: connect your Stripe account to Alterion Labs first. Every event on an unconnected account is dropped, not queued, so skip this step and you get no conversions, no renewals and no reversals from Stripe.

Pass the referral token as client_reference_id on Stripe Checkout. We read it on the completed checkout.

javascript
// When creating the Checkout Session on your server:
const session = await stripe.checkout.sessions.create({
    // ...your line items...
    client_reference_id: alterionToken, // read via alterion.token() on the client
    success_url: "https://your-site.com/thanks?session_id={CHECKOUT_SESSION_ID}",
    metadata: { alterion_order_id: "ORD-1041" },
});

Add the checkout session id to your success URL, as shown above. The tracking script reads it and reports the sale under the same id Stripe uses, so a sale you report from your success page and the same sale arriving from Stripe are recognised as one.

Send your own order reference in the Stripe event metadata as well, under alterion_order_id or order_id. It covers the sale your other path did not report under Stripe's own id: when the two references match within the hour, we merge the two reports into one sale, filling in whatever the first left blank and never overwriting what it already recorded. A sale both paths already report under Stripe's own id is recognised as one before the reference is read, and there the Stripe report corrects the amount your own path sent. Do not put your order reference in client_reference_id; that field carries the referral token. When a Stripe sale arrives with no reference and matches one already recorded under the same customer key (Stripe's own customer id, or the buyer's email when there is none), the same amount and the same currency within the hour, we hold it and ask you rather than paying twice.

Renewals, and refunds and chargebacks that Stripe reports, reach Alterion Labs through Stripe. If your Stripe account is not connected to Alterion Labs, post reversals yourself to the refund endpoint below, and post a cancellation yourself to the endpoint further below the moment a subscription ends. The tracking snippet records what you call it for and nothing on its own: a sale recorded by alterion.convert() is never reversed unless you report the reversal, and a recurring commission is minted only when you report that customer's renewal yourself, through alterion.convert() or the postback. Call it once at checkout and nothing else, and nothing else ever generates, cancellation reported or not. Only a connected Stripe account reports its own renewals for you.

New and existing customers

newCustomer is the one fact only your database knows: send true when this buyer has never paid you before. The line ships commented out on purpose. Alterion Labs does not guess this from your sales with our partners, because we only see the sales partners brought you: a customer who has bought from you ten times directly looks new to us, and pricing them as new is exactly the overpayment this setting exists to prevent. Until it arrives, we fall back to Stripe, then to your campaign's setting. A sale that inherits from a signup carries the status the signup already resolved to. When the signup left it unanswered, the sale resolves it once, the same way, and that answer is final.

Leaving the field out is not the same as sending false. Out means you have not told us, and we fall back to Stripe or to your campaign's setting. Sending false on every call tells us every one of your buyers is a returning customer.

Proving the token survived signup

Store the referral token on the customer record when they sign up, and send it back on the event you pay commission for. Read it with alterion.token() once the script has run; it returns null until the script has written its cookie. This step is optional: a signup already has a marker, the signup event covered above, and a trial that converts within your campaign's signup window attributes through it with no token involved. Set your trial length or turn on the unlimited window, as described above, or that marker covers no more than your cookie window already does.

A token you stored and sent back is a declaration, and it attributes past your cookie window. A token the script read from its own cookie stays inside the window. Send it if you want a second, independent proof that a late-converting trial attributed correctly; the signup event above already gets the sale attributed on its own terms.

On the postback, send it as token. On Stripe Checkout, pass it as client_reference_id when you create the session. Stripe fixes that value at creation and we read it only on the completed checkout, so a token cannot ride a later invoice. A Stripe subscription's renewals and its trial conversion attribute through the customer recorded on that first session instead.

When a postback or a Stripe Checkout session carrying client_reference_id arrives for a customer whose click is older than your cookie window, the install panel confirms the token survived signup, provided that sale resolved its own click rather than inheriting one from a signup event: a sale that inherits its source click from alterion.signup() can never prove this step, however old the click, because that click was never this event's own marker to prove. That proves the install ladder's optional token-persistence step, passable once an event you post resolves a click older than your cookie window. The signup event, covered above, proves the ladder's fourth step instead, and that one is passable before any sale at all.

What the install panel shows

Five steps, in the order the panel shows them: the head script detected, a test conversion received, the installation verified, a referred signup seen, and the token stored on your customer record. The first three are the required path. The last two are the two proofs above, and neither blocks the other: a referred signup can pass its step before your first sale, and once verified the first three never demote again even if a later call goes quiet.

The panel also flags two things it cannot fix for you. "Signup event not installed" appears once your ladder has started and no referred signup has ever reached us; it means exactly what Signups above describes, nothing more. A double-wire notice appears when Stripe is connected and one of your other paths is recording sales with no reference tying it to the Stripe report; it names the paths it saw, not a duplicate payment, because the paths it watches cannot see your Stripe event metadata, and a merchant who sets a shared order reference there, as Stripe Checkout above describes, can see the notice even when correctly wired.

Postback

Fire this from your server on each sale.

GEThttps://alterionlabs.com/api/affiliate/track/postback
POSThttps://alterionlabs.com/api/affiliate/track/postback
text
key=pk_0123456789abcdef01234567&token=9f2c1a4e7b3d&order_id=ORD-1041&payment_ref=cs_test_a1b2c3d4e5f6&amount_cents=4900&currency=USD&type=sale&customer_id=cus_QX1a2b3c&new_customer=1

Sign each request with your postback secret, HMAC-SHA256, sent as x-alterion-signature: sha256=<hex>. On a GET, the signed payload is the canonical query string: every parameter sorted by name, then by value where a name repeats, each name and value percent-encoded with encodeURIComponent, joined with &. It is not the query string in the order you wrote it, and it carries neither the path nor the leading question mark. On a POST, sign the raw JSON body byte for byte.

This is the string that is signed:

text
amount_cents=4900&currency=USD&customer_id=cus_QX1a2b3c&key=pk_0123456789abcdef01234567&new_customer=1&order_id=ORD-1041&payment_ref=cs_test_a1b2c3d4e5f6&token=9f2c1a4e7b3d&type=sale

A bad signature is rejected before the sale is recorded. Fields are validated first, so a malformed request reports the field rather than the signature. An unrecognised key is rejected the same way a bad signature is.

keystring

Public tracking key from the head script tag.

tokenstring

The referral token, lowercased on arrival.

order_idstring

Your own reference for the sale.

payment_refstring

Your payment processor's id for this transaction, such as a Stripe Checkout session or invoice id. Optional. A number or boolean is stringified and treated the same way. A blank, whitespace-only, over-255-character, object or array value is dropped silently, not rejected.

amount_centsinteger

The sale amount in the currency's smallest unit (cents for USD and EUR, yen for JPY). Send 0 for a signup.

currencystring

Three-letter currency code.

typestring

sale, signup or lead. Defaults to sale.

customer_idstring

Your customer id. Enables recurring attribution on a sale, and the identity a later paid event can inherit on a signup.

customer_emailstring

The buyer's email address. On a sale it is not stored as an identity by itself, send customer_id for recurring attribution. On a signup (type=signup) it becomes the identity when customer_id is absent, the same as alterion.signup()'s customerEmail.

codestring

Redeemed referral code, when the buyer typed one instead of following a link. Trimmed and uppercased. An empty or over-32-character value is dropped silently, not rejected.

test"1" | "true" | "0" | "false"

Send 1 or true to mark a test conversion. Case does not matter. 0, false and an empty value all mean a live sale. Any other value is rejected and the sale is not recorded.

new_customerboolean

Whether this buyer has never paid you before.

key, token, order_id, amount_cents and currency are required. The rest are optional. All five are required on type=signup too, even though a signup has neither an order nor an amount: send order_id and amount_cents=0 anyway.

type=signup reaches attribution the same way alterion.signup() does and mints no commission on its own. Send customer_id or customer_email: sending neither is refused, and nothing is recorded.

Add new_customer=1 for a buyer who has never paid you before, or new_customer=0 for one who has. Both 1/0 and true/false are accepted. Leaving the field out is not the same as sending 0: out means you have not told us. Sending 0 on every call tells us every one of your buyers is a returning customer. A value we do not recognise, including an unfilled template variable that renders empty, reads as not declared and never fails the sale.

test reads the same vocabulary, in any case, and an empty value means a live sale. It rejects anything it cannot read, because recording a real sale for a merchant who was testing puts a sale a partner never earned in their balance.

order_id is your own reference for the sale, and it is what deduplicates a sale two paths both report. Send the same value on the postback that you send in your Stripe metadata: the two sides have to match, and the match holds for the hour either side of the sale.

If you have Stripe connected as well, send payment_ref too, carrying the Stripe checkout session or invoice id for the same sale. It is the more exact key. The Stripe path records the sale under that same id, so the two arrivals land on the same key and the second is an ordinary duplicate, and it covers the sale whose Stripe metadata reference is missing. A payment_ref we drop, listed above, leaves the sale recording under your order_id as before.

Without payment_ref, a matching order_id in your Stripe metadata still deduplicates the sale: it merges the two reports, filling in whatever the first left blank and never overwriting what it already recorded, and flags it for you whenever the two disagree on the amount, the currency, the customer or the subscription. Once a commission exists for the sale, filling in a customer or a subscription id is flagged too. Without either, we fall back to matching on buyer, amount and currency within the hour, and that fallback needs a customer key both paths share: send Stripe's customer id, or the buyer's email address, as customer_id on the postback. With no payment_ref, no matching metadata reference and no shared customer key, nothing joins the two rows, and the sale records twice and pays the affiliate twice. A sale that falls outside the match records twice the same way. A match inside it is held for you rather than merged, exactly as on the Stripe path above. Your order_id is kept in every case.

Reversing a commission

Refunds and chargebacks that Stripe reports on a connected account reverse commissions on their own. Everything else needs this endpoint. Without it, a refunded order stays paid.

Post a signed refund the moment your own system records one.

POSThttps://alterionlabs.com/api/affiliate/track/refund

Sign it the same way, sent as x-alterion-signature: sha256=<hex>.

json
{
  "key": "pk_0123456789abcdef01234567",
  "order_id": "ORD-1041",
  "refund_id": "rf_88213",
  "cause": "refund",
  "occurred_at": "2026-03-14T09:12:00Z",
  "amount_refunded_cents": 4900
}

Sign the raw request body with your workspace postback secret, HMAC-SHA256, hex encoded, exactly as you sign a postback. The credential is the same one. A bad signature is rejected before any other field is read, and so is an unrecognised key.

POST only. There is no GET form: a reversal is always server-initiated and does not belong in a query string.

cause is required and closed. One of refund, chargeback, cancellation, merchant_rejection. Your partners see the cause, so an open string would put your internal notes in front of them.

cause: "cancellation" here reverses one commission you have already earned: the sale stands, the customer keeps what they bought, and the affiliate's credit for that one order is taken back. That is a different operation from ending a subscription, further below, which reverses nothing already earned and instead stops future commissions on that chain from generating at all. Reverse one earned commission here. Stop the chain below.

occurred_at is when the refund happened, not when you posted it. Send an ISO-8601 timestamp with an explicit Z or UTC offset; a date alone or a time with no zone is rejected. A nightly batch posting yesterday's refunds records them on yesterday.

refund_id is the idempotency key, scoped to your workspace. Post the same one twice and the second call answers {"ok": true, "outcome": "duplicate"}. It is never an error. A replay of a refund that parked is the exception: it retries the resolution rather than answering duplicate, so re-posting is how you apply one after the sale is recorded.

amount_refunded_cents is optional and is recorded, not acted on. Any refund reverses the commission in full.

Send test: true to check your signature and payload without reversing anything. The response is {"ok": true, "test": true} and no commission is touched.

A refund for an order we have not seen yet is parked, not rejected. The response is {"ok": true, "outcome": "parked", "reason": "conversion_not_found"}, and the reversal applies the moment that conversion is ingested. Resolution is your order id inside your workspace and nothing else. We never guess which sale you meant.

A reversal that applied answers {"ok": true, "outcome": "clawed_back", "reversed_paid_commissions": 0}.

A reversal that is still parked appears on your Conversions screen under "Reversals waiting for a sale", with how long it has been waiting and a retry. Post a refund for an order id you never sent us and it sits there until you fix the reference.

When a reversal reaches a commission you have already paid, reversed_paid_commissions reports how many, and notice states that the reversal reached a commission that was already paid and that the affiliate has been told.

Ending a subscription

This is optional. The cancellations Stripe reports halt a chain's recurring commission on their own, on a campaign configured to pay recurring commissions at all, when your Stripe account is connected. If it is not connected, or you bill outside Stripe entirely, post a cancellation yourself the moment a subscription ends. A merchant who never sends one is exactly where they are today.

This ends future commission generation for that customer's chain. It does not reverse a commission already earned, it does not refund anything, and it does not remove the customer. It also ends this affiliate's exclusive claim on the customer: a later sale from a new referral can be credited to a different affiliate, and clicks from before the date you send stop counting as evidence the customer came back. To reverse a commission you have already paid, use the refund endpoint above with cause: "cancellation".

POSThttps://alterionlabs.com/api/affiliate/track/cancellation

Sign it the same way, sent as x-alterion-signature: sha256=<hex>.

json
{
  "key": "pk_0123456789abcdef01234567",
  "customer_id": "cus_QX1a2b3c",
  "subscription_id": "sub_1Mz9AbCdEf",
  "occurred_at": "2026-03-14T09:12:00Z"
}

Sign the raw request body with your workspace postback secret, HMAC-SHA256, hex encoded, exactly as you sign a postback or a refund. The credential is the same one. A bad signature is rejected before any other field is read, and so is an unrecognised key.

POST only. There is no GET form: a cancellation is always server-initiated and does not belong in a query string.

customer_idstring

Your customer id. At least one of customer_id or customer_email is required; sending neither is rejected.

customer_emailstring

The buyer's email address, used when customer_id is not available.

subscription_idstring

The subscription that ended. Optional, and worth sending: it lets this report reach a chain recorded under a different customer id for the same subscription, and it narrows the halt to that one subscription rather than every commission this customer earns.

occurred_atstring

When the subscription actually ended, not when you posted this. An ISO-8601 timestamp with an explicit Z or UTC offset; a date alone or a time with no zone is rejected. It cannot be in the future, with five minutes of clock-skew tolerance: send the date the subscription ended, not when it is scheduled to.

testboolean

Send true to check your signature and payload without halting anything. The response is {"ok": true, "test": true} and nothing is recorded.

Report the cancellation under an identifier you already sent on a conversion for this customer: the same customer_id, the same email, or the subscription id. A report under a different identifier does not reach that customer's chain, and nothing tells you why: the response still answers ok, and the commission keeps generating exactly as though nothing had been sent. Re-post it under the identifier you actually sent on the sale.

A cancellation for a customer we have not recorded a sale for yet is accepted, not an error. The first sale we record for that customer still earns its commission, and this cancellation bounds the renewals after it. You do not need to know which of your customers we have already seen.

A cancellation that lands answers {"ok": true, "outcome": "recorded"}. The exact same report, the same identifiers and the same occurred_at, answers {"ok": true, "outcome": "duplicate"} on a later call. Changing occurred_at, or a later call that resolves a new alias for this customer, answers recorded again rather than duplicate, because it is genuinely new information for the chain. Neither is an error.

Every cancellation we record here, from Stripe or from this endpoint, also feeds Customer churn on your Analytics screen. A connected Stripe account makes that number live immediately, even with zero cancellations so far. Without Stripe connected, Customer churn appears once you send your first cancellation here; until then, Analytics tells you cancellation data is not connected instead of guessing.

Privacy guidance

Our tracking script observes visitor behaviour on your own site. You are the data controller and Alterion Labs is the processor. This section is what you need in your own privacy notice.

What is stored on a visitor's device

A redirect affiliate link sets two cookies from our server once a visitor opens it. alterion_ref_<campaignId> holds the referral for that campaign and lasts for the campaign's cookie window. alterion_cid holds a random device identifier and lasts 400 days. Both are httpOnly, so no script on your page can read them. A browser prefetching the link in the background sets neither cookie until a visitor actually opens it.

alterion_ref holds the referral: which partner sent the visitor, and the identifiers that let a later sale resolve back to them. It is written only on a visit that arrives through an affiliate link, and it lasts for your campaign's cookie window.

alterion_seen holds prior activity: when the visitor was first seen on your site, when they were last seen, and how many separate visits that adds up to. It is written on ordinary visits that carry no referral link, and it lasts sixty days. Its contents never leave the device on an unreferred visit, and no record of that visitor exists on Alterion Labs' side unless and until they arrive through an affiliate link. The script does fetch its configuration from Alterion Labs on every page load, referred or not. That request carries your public key and, as any web request does, the visitor's IP address and user agent. It records that your installation is live, and it creates no visitor record.

alterion_probe is set with a ten-second lifetime, then read back and deleted immediately, when the script needs to establish whether the browser keeps cookies at all. It carries no information about the visitor.

The three the script writes are first-party cookies on your own domain. The two the redirect sets belong to whichever domain serves your referral links. Until Alterion Labs provisions the certificate and the CDN alias for your own tracking domain, that is the Alterion Labs platform origin, so those two are third-party cookies from your site's point of view. Connecting your own tracking domain makes them first-party. The two the script writes for the referral and for prior activity are scoped to your own domain and are never read on another merchant's site. The device identifier the redirect sets belongs to the domain that serves referral links, which today is shared across every Alterion Labs merchant, so the same cookie is read when that visitor follows another merchant's link. What we record on that merchant's click is derived from it with a key unique to their workspace, so one merchant's record of a device and another's cannot be compared. Merchants never see one another's records. The device signature described below is hashed separately for each workspace and cannot be compared across them.

What is collected

The script records whether the referred page loaded and how long it was visible, up to sixty seconds. It records no scroll depth, no keystrokes, and no pointer or scroll event counts. It records a coarse device signature made of the browser and operating system family, the language list, the timezone offset, a rounded screen size and a processor count collapsed into three bands. No canvas, audio or font fingerprinting is used. Raw IP addresses are never stored in your program's records. The address is used in memory to classify the request and to rate-limit it, and only a salted hash is kept.

When a visitor arrives through a direct affiliate link, the script sends the address of the page they landed on, including its query string, and the address of the page that referred them. Both are recorded on the click. The arrival and engagement reports send the page path on its own, without the query string, and version 2 does not store it.

On a referred arrival we also record what any web server sees: the browser's user agent string, its language header, its fetch-metadata headers, and the country the visitor's network resolves to. On a conversion, the script sends what you passed to alterion.convert(), meaning your order id, the amount, the currency, your customer id, and, when you send it, the customer's email address, which is stored only as a hash, alongside the referral token and cookie identifier it reads from its own cookie, the coarse device signature described above, and whether you marked the call a test or declared the buyer new or returning. A signup sends the same referral token, cookie identifier and device signature, plus whichever of a customer id or an email you passed to alterion.signup(), from an ordinary post-signup page view, not only a paid one. The email is stored only as a hash there too.

On a referred arrival it also reports what alterion_seen held, when it can: we record three facts on the click, whether there had been prior activity, when it started, and how many visits it covered, unless storage is blocked, in which case none of the three is recorded. A redirect link reports the same three facts on the arrival beacon, since that click is already recorded server-side, or on the visit report when the script sends one. We do not record which pages were seen.

The hashes and their keys

The device signature is hashed with a secret key and your workspace's own identifier before it is stored, so the stored value differs in every workspace and cannot follow a visitor to another merchant. A customer email address that arrives on a conversion or a signup is hashed the same way. Neither hash is reversible and neither is shared.

What to disclose

Name the five cookies and what each holds, and say that two of them are third-party cookies served from the Alterion Labs platform domain until you connect your own tracking domain, and that the device identifier cookie is set on that same domain for every merchant until then, while the value recorded against your account is derived for your workspace alone. Say that a third party processes referral and conversion data on your behalf. Say that how long a referred page stayed visible, up to sixty seconds, is recorded. Say that a coarse device signature is collected and hashed for fraud detection. Say that the landing page address, including its query string, and the referring page address are recorded on a referred arrival. Say that prior activity on your site is recorded on a referred arrival: whether there had been any, when it started, and how many visits it covered. Say that a customer identifier or email address you send us when a visitor signs up is recorded too, from an ordinary post-signup page view, with an email address stored only as a hash the same way a conversion's is. Alterion Labs' own privacy policy describes the processing in Section 11.

Brand & Affiliate API

Read your program data

Read your brand profile, program stats, and affiliates over REST. These endpoints are live today. An agent with brand.read and affiliate.read can pull everything it needs to report on your program. Monetary fields are integers in the currency's smallest unit (cents for USD and EUR, yen for JPY), each paired with its currency code. Amounts in different currencies are never added together.

Objects

The brand object describes your account; the affiliate object describes a partner in your program.

idstring

Unique brand identifier.

slugstring

URL-safe brand handle.

namestring

Brand name.

planstring

Subscription plan.

websiteUrlstring

Program website. Null until a campaign is active.

campaignobject

Active campaign { name, rewardType, rewardValue, newCustomerRewardValue, firstPaymentRewardType, firstPaymentRewardValue, lateRewardValue, lateRewardAfterMonths, maxCommissionMonths, maxCommissionsPerCustomer, recurring, daysUntilDue, paymentTermsNetDays, paymentTermsCadence, paymentTermsPayDay, paymentTermsNote }, or null. newCustomerRewardValue is the new-customer rate, or null when the campaign pays one rate; when it is set, rewardValue is the existing-customer rate, not the only rate. firstPaymentRewardType and firstPaymentRewardValue are an absolute override for the first payment, or both null when there is none. lateRewardValue is the reduced rate the campaign moves into lateRewardAfterMonths months after the referral's first payment, or null when there is no reduced rate. maxCommissionMonths is a hard stop on commissioned months, or null when there is none. maxCommissionsPerCustomer is a hard stop on commissioned payments, or null when there is none. recurring is false on a campaign that pays once; the reduced rate and both caps never apply there, whatever lateRewardValue, maxCommissionMonths, and maxCommissionsPerCustomer say. daysUntilDue is the hold: how many days after a sale a commission becomes eligible, once the merchant also approves the referral. Eligibility needs both the hold to elapse and the merchant's review, never the elapsed days alone. When the merchant states no explicit hold, it defaults to the campaign's own return window. paymentTermsNetDays and paymentTermsCadence are the payment clock: what the merchant commits to after eligibility, in days and in a recurring cadence (weekly, twice_monthly, or monthly), both null when the merchant has not stated a schedule. paymentTermsPayDay is the day of that cadence paid on (the ISO weekday, 1 Monday to 7 Sunday, for weekly; the day of month, 1 to 28, otherwise), null for the cadence's own default anchor. paymentTermsNote is free text for anything the structured fields can't say, or null.

createdAttimestamp

ISO 8601 creation time.

Endpoints

GET/v1/brand

Retrieve your brand profile. Scope: brand.read.

bash
curl https://alterionlabs.com/v1/brand \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# Response
{
  "brand": {
    "id": "9f2c81e4-3a6b-4d2f-8c10-1e7a9b0c4d5e",
    "slug": "acme",
    "name": "Acme",
    "plan": "pro",
    "websiteUrl": "https://acme.com",
    "createdAt": "2026-01-12T09:30:00.000Z",
    "campaign": {
      "name": "Acme Partners",
      "rewardType": "percentage",
      "rewardValue": 20,
      "currency": "USD",
      "newCustomerRewardValue": null,
      "firstPaymentRewardType": null,
      "firstPaymentRewardValue": null,
      "lateRewardValue": null,
      "lateRewardAfterMonths": null,
      "maxCommissionMonths": null,
      "maxCommissionsPerCustomer": null,
      "recurring": true,
      "daysUntilDue": 30,
      "paymentTermsNetDays": null,
      "paymentTermsCadence": null,
      "paymentTermsPayDay": null,
      "paymentTermsNote": null
    }
  }
}
GET/v1/brand/stats

Program totals: clicks, conversions, and one entry per currency with revenue and commissions. Conversions count approved and pending sales only, never rejected, refunded, test, setup or signup rows. Scope: brand.read.

bash
curl https://alterionlabs.com/v1/brand/stats \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# Response
{
  "stats": {
    "clicks": 18342,
    "conversions": 263,
    "conversionRate": 0.0143,
    "activeAffiliates": 47,
    "totals": [
      { "currency": "EUR", "revenueCents": 1210000, "commissions": { "paidCents": 98000, "pendingCents": 20400 } },
      { "currency": "USD", "revenueCents": 4820000, "commissions": { "paidCents": 612000, "pendingCents": 88400 } }
    ]
  }
}
GET/v1/affiliate

List your affiliates (up to 100, newest first). Scope: affiliate.read.

bash
curl https://alterionlabs.com/v1/affiliate \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# Response
{
  "affiliates": [
    {
      "id": "a1b2c3d4-5e6f-4789-90ab-cdef01234567",
      "email": "partner@example.com",
      "firstName": "Jordan",
      "lastName": "Lee",
      "accountStatus": "active",
      "createdAt": "2026-03-04T14:08:00.000Z"
    }
  ]
}
GET/v1/affiliate/stats

Per-affiliate clicks, conversions, and earnings, one entry per currency. Conversions count approved and pending sales only, never rejected, refunded, test, setup or signup rows. Earnings count pending, approved and paid commissions, never a reversed one. Scope: affiliate.read.

bash
curl https://alterionlabs.com/v1/affiliate/stats \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# Response
{
  "affiliates": [
    {
      "affiliateId": "a1b2c3d4-5e6f-4789-90ab-cdef01234567",
      "email": "partner@example.com",
      "name": "Jordan Lee",
      "clicks": 1203,
      "conversions": 21,
      "earnings": [
        { "currency": "EUR", "amountCents": 20000 },
        { "currency": "USD", "amountCents": 84300 }
      ]
    }
  ]
}

Changes

Stats are per currency. GET /v1/brand/stats no longer returns currency, revenueCents and commissions at the top level. It returns totals, one entry per currency with currency, revenueCents and commissions. GET /v1/affiliate/stats no longer returns earningsCents. Each affiliate returns earnings, one entry per currency with currency and amountCents.

Blog API

Publish and manage content

A REST API for the Content Marketing Agent's blog engine. Create and update posts, pull feeds, read engagement, and manage newsletter subscribers, all programmatically.

The Post object

idstring

Unique identifier, prefixed post_.

titlestring

Post title.

slugstring

URL slug. Generated from the title if omitted.

statusenum

One of draft, scheduled, or published.

bodystring

Post content in Markdown.

excerptstring

Short summary. Auto-generated if omitted.

tagsstring[]

Topic tags.

authorobject

{ name, url }.

seoobject

{ title, description } for search and social.

published_attimestamp

ISO 8601 publish time. Null for drafts.

created_attimestamp

ISO 8601 creation time.

updated_attimestamp

ISO 8601 last-update time.

Endpoints

POST/v1/posts

Create a post. Returns the created Post object.

bash
curl https://alterionlabs.com/v1/posts \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "How we cut churn 30%",
    "body": "## The problem\n...",
    "tags": ["retention", "saas"],
    "status": "published"
  }'
GET/v1/posts

List posts. Filter with status and tag; paginate with limit and starting_after. Returns { data, has_more }.

bash
curl "https://alterionlabs.com/v1/posts?status=published&limit=20" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
GET/v1/posts/{id}

Retrieve a single post by id.

bash
curl https://alterionlabs.com/v1/posts/post_8fk2 \
  -H "Authorization: Bearer $ACCESS_TOKEN"
PATCH/v1/posts/{id}

Update a post. Send only the fields you want to change.

bash
curl -X PATCH https://alterionlabs.com/v1/posts/post_8fk2 \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "status": "draft" }'
DELETE/v1/posts/{id}

Delete a post. Returns { id, deleted: true }.

bash
curl -X DELETE https://alterionlabs.com/v1/posts/post_8fk2 \
  -H "Authorization: Bearer $ACCESS_TOKEN"
GET/v1/feed

Pull the published feed. Set format to rss or json.

bash
curl "https://alterionlabs.com/v1/feed?format=json" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
GET/v1/posts/{id}/metrics

Read engagement. Returns { views, unique_visitors, comments, avg_read_time }.

bash
curl https://alterionlabs.com/v1/posts/post_8fk2/metrics \
  -H "Authorization: Bearer $ACCESS_TOKEN"
POST/v1/newsletter/subscribers

Add a subscriber to your newsletter. Returns the subscriber object.

bash
curl https://alterionlabs.com/v1/newsletter/subscribers \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "email": "reader@example.com", "name": "Alex" }'

Content Agent API

Run the agent from your stack

Call the AI Content Marketing Agent directly. Generation runs asynchronously: you create a job, poll until it completes, then schedule or publish the resulting post.

The Job object

idstring

Unique identifier, prefixed job_.

statusenum

One of queued, processing, completed, or failed.

typestring

The job type, e.g. generate.

resultobject

{ post_id } when completed; null otherwise.

errorobject

Error detail when failed; null otherwise.

created_attimestamp

ISO 8601 creation time.

completed_attimestamp

ISO 8601 completion time. Null until finished.

Endpoints

POST/v1/content/generate

Generate a draft from a topic. tone defaults to professional, length to medium. Returns a Job with status queued.

bash
curl https://alterionlabs.com/v1/content/generate \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "topic": "Reducing SaaS churn with onboarding",
    "keywords": ["saas churn", "user onboarding"],
    "tone": "professional",
    "length": "medium",
    "target_url": "https://yourapp.com"
  }'
GET/v1/content/jobs/{id}

Retrieve a job. Poll until status is completed, then read result.post_id.

bash
curl https://alterionlabs.com/v1/content/jobs/job_q1z9 \
  -H "Authorization: Bearer $ACCESS_TOKEN"
POST/v1/content/schedule

Schedule a post for publication at a future time.

bash
curl https://alterionlabs.com/v1/content/schedule \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "post_id": "post_8fk2",
    "publish_at": "2026-07-01T09:00:00Z"
  }'
POST/v1/content/publish

Publish a post now to a destination. destination.type is one of blog, webhook, or cms.

bash
curl https://alterionlabs.com/v1/content/publish \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "post_id": "post_8fk2",
    "destination": { "type": "webhook", "url": "https://yourapp.com/hooks/blog" }
  }'
GET/v1/content/calendar

List scheduled content. Filter by month (YYYY-MM). Returns { data: [{ post_id, title, publish_at, status }] }.

bash
curl "https://alterionlabs.com/v1/content/calendar?month=2026-07" \
  -H "Authorization: Bearer $ACCESS_TOKEN"