# PostbackX Documentation — complete text > How to track, attribute and report on your ad campaigns with PostbackX. Every documentation page, concatenated, so this file can be read in one fetch. Canonical HTML lives at https://docs.postbackx.com. --- # Overview _What PostbackX does, the five steps traffic moves through, and where to start._ Source: https://docs.postbackx.com/ PostbackX sits between your ads and your offers. It records every click, matches each conversion back to the click that caused it, and reports revenue against ad spend — so you can tell which campaign, ad set and creative actually made money. This documentation is for the people running those campaigns. ## The shape of the thing Five moving parts, in the order traffic moves through them: 1. **A campaign** gives you one tracking link to put behind your ads. 2. **A click** on that link is recorded, then redirected onward. 3. **A landing page or a flow** decides where the visitor lands. 4. **The visitor converts** on your offer. 5. **Your offer source calls a postback URL**, and PostbackX matches that conversion to the original click — then reports it and forwards it to the ad platform's pixel so its algorithm learns. If something is wrong, it is almost always step 5: the conversion arrived without a click id, or arrived at the wrong URL, and had nothing to attach to. That is what [Conversions are missing](/guides/missing-conversions) is about. ## Getting started If you are setting up an account for the first time, do it in this order — each step needs the one above it: - [Your first campaign](/guides/first-campaign) — the whole path end to end - [Connecting an offer source](/guides/offer-sources) — so conversions can get back in - [Sending conversions back to ad platforms](/guides/pixels) — so the algorithm optimizes for buyers - [Custom tracking domains](/guides/tracking-domains) — so links carry your own name ## If you would rather ask than read PostbackX exposes your own reporting data to AI assistants over [MCP](/reference/mcp). Connect it once and you can ask, in a normal sentence, which campaigns lost money last week — instead of building the report yourself. There is also a [REST API](/reference/api) with an [OpenAPI specification](https://api.postbackx.com/v1/openapi.json), if you are building something that reads your numbers on a schedule. > These docs are written to be read by assistants too. Add `.md` to any page URL > for its Markdown source, and see [llms.txt](/llms.txt) for the index. ## Reference - [Vocabulary](/concepts/vocabulary) — what each word means here, and which words mean two things elsewhere - [How attribution works](/concepts/attribution) — click ids, matching windows and why a conversion goes missing - [Postback parameters](/reference/postback) — every parameter the postback URL accepts - [Tracking snippets](/reference/snippets) — the JavaScript for landing pages and direct links --- # Using these docs with AI _Three ways to put this documentation in front of an AI assistant — copy it, point it at a URL, or connect it directly._ Source: https://docs.postbackx.com/ai This documentation is written to be read by assistants as well as people. Every page has a Markdown twin, and the whole set is small enough to hand over in one go — so you can ask questions about PostbackX instead of hunting for the page that answers them. Three ways to do it, from least setup to most useful. ## Copy the whole thing All twelve pages, about 12,000 tokens. That fits comfortably in any current assistant's context window, so it can answer questions spanning several pages at once — how attribution relates to postbacks, say, or what to check when conversions go missing. Copy the whole documentation set from https://docs.postbackx.com/llms-full.txt Paste it into a new conversation and ask your question. There is nothing to configure and no account involved. ## Give it the URL If your assistant can browse the web, it does not need the paste: ``` https://docs.postbackx.com/llms-full.txt ``` That single file is the entire documentation set as plain text. Ask it to read that, then ask your question. ## Connect it to your account The other two options give an assistant the *documentation*. This one also gives it **your numbers**. PostbackX runs an [MCP server](/reference/mcp). Once connected, an assistant can read your campaigns, spend and conversions directly — and it can read this documentation too, since the docs are exposed as resources on the same connection. No copying, and nothing to keep up to date. That combination is what makes the difference: an assistant that knows both what ROAS means here *and* what yours was last week can answer questions neither source could alone. ## The machine-readable files | File | What it is | Size | | --- | --- | --- | | [`/llms.txt`](/llms.txt) | Index of every page with one-line summaries. For picking what to read. | ~1,000 tokens | | [`/llms-full.txt`](/llms-full.txt) | Every page, concatenated. For reading in one fetch. | ~12,000 tokens | | `.md` | Any single page as Markdown — add `.md` to its URL. | ~1,000 tokens | | [OpenAPI spec](https://api.postbackx.com/v1/openapi.json) | The REST API's endpoints and parameters. Describes the API, not this prose. | — | Each page's Markdown twin keeps its frontmatter, so a title, description and `updated` date travel with the text. ## Worth knowing **A copy is a snapshot.** Documentation you pasted last month describes the product as it was last month. Every page carries an `updated` date so an assistant can tell you how old its information is — and re-copying costs one click. **Never take figures from documentation.** Payouts, spend, ROAS and conversion counts in these pages are illustrative examples, not your data. An assistant reading only the docs will happily quote them at you. For real numbers it needs [a connection to your account](/reference/mcp) or the [API](/reference/api). **Check the vocabulary.** This industry uses the same words for different things, and an assistant trained on the wider web tends to say "network" where PostbackX says [offer source](/concepts/vocabulary). If an answer sounds subtly off, that is usually why. --- # Vocabulary _What each word means in PostbackX, including the four that mean something different in the rest of the industry._ Source: https://docs.postbackx.com/concepts/vocabulary Ad tracking has a vocabulary problem: the same word means different things at different companies, and two different words often mean the same thing. This page is the list PostbackX uses. Where the industry disagrees with us, that is noted. ## The two kinds of "source" This is the distinction worth learning first, because both sides of your business get called a source somewhere. **Platform** is where your ad spend goes — the traffic side. Facebook, NewsBreak, TikTok, Snapchat, Roku, Axon, BidSystem and Google are platforms. When a report asks you to filter by platform, it means this. **Offer source** is where your offers come from — the money side. It is the affiliate platform that pays you, and the thing that sends conversions back to PostbackX. Everflow is an offer source. > Most of the industry calls an offer source a "network". PostbackX does not, > because "network" also names an ad placement (Facebook's Audience Network) and a > traffic partner. The REST API still uses `network_id` and `network_name` in field > names for historical reasons — the field is an offer source. ## Traffic, in order **Campaign** — the thing that ties everything together. A campaign takes a pixel, an offer and optionally a flow, and gives you one tracking link. Every click, conversion and pixel event that follows is attributed through it. **Click** — one visit to your tracking link. PostbackX records it, assigns it a **click id**, and redirects the visitor onward. The click id is the thread that the whole system pulls on later; see [attribution](/concepts/attribution). **Offer** — what you are promoting, and where the visitor converts. It has a destination URL and a payout. **Landing page** — a page of your own that the visitor sees before the offer. Worth using when you want to warm the visitor up, or when you need Facebook's browser cookies (`_fbp` and `_fbc`), which can only be read client-side and materially improve Facebook's match quality. > The code and some report titles say "lander". Same thing. **Flow** — a reusable routing rule: which landing pages and offers traffic should be split across, and in what proportion. Define one, use it in many campaigns, change it once and every campaign follows. Sometimes called a funnel. **Direct link** — the alternative to redirecting. Your ad points straight at your landing page, and a JavaScript snippet creates the click record from the page itself. Use it when you want full control of the URL in the ad, or want to avoid redirect latency. See [tracking snippets](/reference/snippets). ## Money, in order **Conversion** — the visitor did the thing: submitted a form, bought, called. Your offer source tells PostbackX by calling a [postback URL](/reference/postback). **Payout** — what you earn for one conversion. Revenue is payout summed. **Spend** or **cost** — what the ad platform charged. Synced automatically from each connected platform. **Transaction id** — the offer source's own id for a conversion. PostbackX uses it to recognise a duplicate delivery, so the same conversion is not counted twice. ## Metrics These are computed for you, everywhere, the same way. Do not recompute them from the raw fields — if a report and your spreadsheet disagree, this is usually why. | Metric | Definition | Reading it | | --- | --- | --- | | Profit | `revenue - spend` | Negative means you are paying for traffic that does not pay back. | | ROAS | `revenue / spend` | Return on ad spend. **1.0 is break-even**, not zero. | | Conversion rate | `conversions / clicks` | | | EPC | `revenue / clicks` | Earnings per click. The number to compare against your CPC. | | CPA | `spend / conversions` | Cost per acquisition. Compare against payout. | When there is nothing to divide by — no spend, no clicks — the result is `0` rather than an error or a blank. ## Getting conversions in and out **Postback** — a URL your offer source calls when a conversion happens. This is how conversions get *into* PostbackX. It carries the click id, so PostbackX knows which click to credit. **Pixel** — how conversions get *out* of PostbackX, back to the ad platform, so its algorithm can optimize toward people who buy rather than people who click. For Facebook this uses the Conversions API. **CAPI** — Facebook's Conversions API: server-to-server conversion reporting, as opposed to the browser pixel. More reliable, and it works when the browser blocks things. **Campaign mapping** — the link between a PostbackX campaign and the ad platform's own campaign. Mapping them is what lets PostbackX put your spend and your revenue in the same row. --- # How attribution works _How a conversion finds its way back to the click that caused it, and the four places that chain can break._ Source: https://docs.postbackx.com/concepts/attribution Attribution is one question: *which click caused this conversion?* Everything PostbackX reports depends on answering it, and almost every reporting problem is really this answer having gone missing. ## The chain A **click id** is created when someone clicks your ad, and it is carried through every step that follows. 1. Someone clicks your ad and arrives at your tracking link. PostbackX writes a click record and generates a click id. 2. PostbackX redirects them to your offer, putting the click id into the offer URL — that is what the `{click_id}` macro in your offer URL is for. 3. Your offer source stores that value alongside the visitor's session, usually in a field named `sub1` through `sub5`. 4. The visitor converts. Your offer source calls the [postback URL](/reference/postback) and passes the click id back. 5. PostbackX looks up that click, attaches the conversion to it, and now knows the campaign, ad set, ad, landing page and offer that produced the sale. Step 5 is only possible because of step 2. If the click id was never put into the offer URL, there is nothing to hand back, and the conversion arrives as an orphan. **How a conversion finds its click** — The click id is the thread the whole system pulls on. Here is where it travels. 1. **The click.** Someone clicks your ad and lands on your tracking link. PostbackX records the click and generates a click id. 2. **Into the offer URL.** PostbackX redirects onward, substituting that click id into your offer URL where you put the macro. 3. **The conversion.** The visitor converts. Your offer source calls the postback URL and hands the click id back. 4. **Joined up.** PostbackX looks up that click and attaches the conversion — so it now knows the campaign, ad and creative that earned it. ## What attribution buys you Once the conversion is joined to the click, PostbackX knows everything the click knew. That is what makes a report possible at all: spend comes from the ad platform keyed on the ad, revenue comes from the conversion keyed on the click, and the click id is the only thing that connects them. It is also what makes the [pixel](/guides/pixels) useful. Facebook does not learn anything from a conversion it cannot tie to the ad that caused it, and the click id is how PostbackX tells it. ## Where the chain breaks Four failures, in the order they are common. ### The click id never reached the offer Your offer URL is missing its `{click_id}` macro, so step 2 silently does nothing. Conversions happen and none of them come back with an id. This is the most common setup mistake and it looks exactly like "tracking is broken". The signature: real conversions in your offer source's dashboard, nothing in PostbackX. ### The wrong parameter came back Your offer source stores the click id in `sub5`, but its postback is configured to send `sub1`. The postback arrives with an empty click id and is rejected. The signature: postbacks arriving, all of them failing on a missing click id. ### The postback goes to the wrong place The URL in your offer source's postback field has a typo, points at a host that does not resolve, or was copied from documentation instead of from your account. Nothing arrives at all. ### The click is too old A click expires after **90 days**. A conversion referencing a click older than that is refused, because at that distance the attribution would be a guess. ## Conversions counted twice, and not Offer sources retry. A network that does not get a clean response will send the same conversion again — sometimes for days. PostbackX deduplicates on the combination of **click id, transaction id and event name**. Send a transaction id and repeat deliveries collapse into one conversion, which is why it is worth passing even though it is optional. Without a transaction id, two genuine conversions on the same click and same event are indistinguishable from one conversion delivered twice — so both get counted as one. ## Two events on one click A visitor can convert more than once: a lead, then a sale. Those are separate events on the same click, and both are attributed to it, which is why the event name is part of the dedup key. Configure the events you care about on the campaign and pass `event_name` on the postback. ## Two ways a click gets created The chain above assumes the visitor passes *through* PostbackX and is redirected. That is the default, and it is the most reliable. The alternative is a [direct link](/reference/snippets): your ad points straight at your landing page and a JavaScript snippet creates the click from the page itself. The attribution chain is identical from step 3 onward, but step 1 now depends on the visitor's browser running your snippet — so a blocked script means no click record. The trade is control of the ad URL and no redirect hop, against that dependency. --- # Your first campaign _The whole path from a connected ad account to a tracking link that reports revenue against spend._ Source: https://docs.postbackx.com/guides/first-campaign This is the end-to-end setup, in dependency order. Each step needs the one before it, which is why campaigns come last rather than first — a campaign ties the other pieces together and there is nothing to tie until they exist. Each of these pages has an animated walkthrough built into it. Look for **How it works** in the page header for a step-by-step replay of the screen you are on. ## 1. Connect the ad account [Ad Accounts](https://app.postbackx.com/ad-accounts) — connect the platform you buy traffic on. This is what pulls your ad spend in; without it PostbackX knows your revenue but not what you paid for it, and every profit column reads as pure profit. ## 2. Connect an offer source [Offer Source](https://app.postbackx.com/offer-source) — the affiliate platform your offers come from, and the thing that will send conversions back. Connecting it gives you the postback URL to configure on their side. See [connecting an offer source](/guides/offer-sources). ## 3. Add a pixel [Pixels](https://app.postbackx.com/pixels) — how conversions get sent back to the ad platform, so its algorithm can optimize toward people who buy rather than people who click. For Facebook you need the Pixel ID and a Conversions API access token, both from Events Manager. See [sending conversions back](/guides/pixels). ## 4. Create the offer [Create Offers](https://app.postbackx.com/create-offers) — what you are promoting. If you use Everflow, import it and the configuration is filled in for you. Otherwise use the Custom tab, where you need two things from your offer source: - **The offer URL** — their tracking link. **Append your click id macro**, e.g. `&sub5={click_id}`. Without it, no conversion can be attributed to anything. This is the single most common setup mistake. - **Their postback configured** to fire at PostbackX when a conversion happens. ## 5. Optionally, a landing page or a flow Not required — a campaign can send traffic straight to an offer. [Landing Pages](https://app.postbackx.com/landing-pages) is worth it on Facebook specifically: a page of your own can capture the `_fbp` and `_fbc` browser cookies that only exist client-side, which measurably improves match quality. It needs a [snippet](/reference/snippets) embedded. [Flows](https://app.postbackx.com/flows) are for splitting traffic across several landing pages or offers by weight. Define one and reuse it across campaigns; change it once and every campaign follows. ## 6. Create the campaign [Create Campaigns](https://app.postbackx.com/create-campaigns) — pick the pixel, the offer, and optionally the flow. You get one tracking link. **What a campaign ties together** — A campaign is the knot: a pixel, an offer, optionally a flow — and one link for your ads. 1. **Pick the pixel.** Choose the pixel for the platform you are buying traffic on, so conversions get reported back to it. 2. **Pick the offer.** Choose what you are promoting. This is where the traffic converts. 3. **Create it.** A flow is optional — add one when you want to split traffic across several landing pages or offers. 4. **Your tracking link.** You get one link. Put it behind your ads and every click, conversion and pixel event is attributed automatically. That link goes behind your ads. From then on every click, conversion and pixel event is attributed automatically. ## 7. Map the campaign [Campaign Mappings](https://app.postbackx.com/campaign-mappings) — link your PostbackX campaign to the ad platform's own campaign. This is what puts spend and revenue in the same row. Unmapped, you get two half-reports. PostbackX suggests matches by name; accept the correct ones. ## 8. Verify before you scale Do not wait for real traffic to find out whether it works. 1. Click your own tracking link. It should appear in [Clicks](https://app.postbackx.com/clicks) within seconds. 2. Confirm the click id is visible in the URL you land on. If not, step 4's macro is missing. 3. Fire a test conversion with that click id — the `curl` in [conversions are missing](/guides/missing-conversions) — and confirm it appears in [Conversions](https://app.postbackx.com/conversions). 4. Check the pixel event reached the ad platform. For Facebook that is Events Manager, where it should arrive within minutes. If all four pass, the chain is sound and you can spend money against it. ## Worth knowing early **Use a [custom tracking domain](/guides/tracking-domains).** Links on your own domain look like yours and are more resilient than a shared default. **Send `transaction_id` on your postbacks.** It is optional and it is what makes duplicate detection correct. See [attribution](/concepts/attribution). **Conversions are attributed to the day of the click, not the sale.** Today's numbers keep filling in for days. This is correct, and it surprises everyone once. --- # Connecting an offer source _How to connect the affiliate platform your offers come from, and configure its postback so conversions reach PostbackX._ Source: https://docs.postbackx.com/guides/offer-sources An offer source is the affiliate platform your offers come from and that pays you. Connecting one lets PostbackX receive its conversion postbacks, and send conversion data back to it. > Most of the industry calls this a "network". PostbackX does not, because that word > also names ad placements and traffic partners. See > [vocabulary](/concepts/vocabulary). ## Connect it Go to [Offer Source](https://app.postbackx.com/offer-source) and either pick your platform from the list, or add a custom source with its postback URL. The page has a **How it works** walkthrough that replays the Everflow flow step by step. Authentication, timeouts and retry behavior can be tuned per source once it exists. ## Configure the postback on their side This is the part that happens in *their* dashboard, and the part that usually goes wrong. **Setting up an offer source postback** — What you do inside your offer source, once — and what each field means. 1. **Open your offer.** In your offer source, open the offer you are going to run and find its tracking link. 2. **Append the click id.** Add your click id macro to the tracking link. Without this, no conversion can ever be matched to a click. 3. **Open postback settings.** Switch to the offer’s postback tab. This is how the offer source tells PostbackX that a conversion happened. 4. **Paste the PostbackX URL.** Paste the postback URL. The click id parameter must read back the same slot you used above. 5. **Save.** Save. Conversions now arrive at PostbackX, matched to the click that caused them. PostbackX gives you a URL shaped like this: ``` https://postbacks.postbackx.com/postback?click_id={click_id}&payout={payout} ``` You paste it into your offer source's postback or pixel settings, replacing the `{...}` parts with **their** macros. Every platform names them differently — copy them from their documentation. The critical one is the click id. Whichever parameter carried `{click_id}` in your offer URL is the one the postback must read back: | In your offer URL you appended | So the postback must send | | --- | --- | | `&sub5={click_id}` | `click_id={sub5}` | | `&sub1={click_id}` | `click_id={sub1}` | | `&aff_sub={click_id}` | `click_id={aff_sub}` | Get this pair wrong and postbacks arrive with an empty click id, which PostbackX rejects with a `400`. Nothing else about your setup will look broken. ## Also send a transaction id Optional, and worth doing: ``` https://postbacks.postbackx.com/postback?click_id={sub5}&payout={payout}&transaction_id={transaction_id} ``` Offer sources retry. The transaction id is how PostbackX tells a retry of one conversion apart from two genuine conversions on the same click. Without it, the two are indistinguishable and get counted once. See [attribution](/concepts/attribution). ## Verify it Do not wait for real traffic. 1. Click your own tracking link, and find the click id in [Clicks](https://app.postbackx.com/clicks). 2. Fire a postback by hand with that id: ```bash curl -i "https://postbacks.postbackx.com/postback?click_id=REAL_CLICK_ID&payout=1.00&event_name=lead&transaction_id=manual-test-1" ``` A `200` with a `conversion_id` means PostbackX is ready and anything still missing is on your offer source's side. Full parameter and response reference: [postback parameters](/reference/postback). If conversions still do not arrive once traffic is live, work through [conversions are missing](/guides/missing-conversions). --- # Conversions are missing _A diagnostic order for when your offer source shows conversions and PostbackX does not._ Source: https://docs.postbackx.com/guides/missing-conversions Your offer source reports conversions. PostbackX shows fewer, or none. This is the most common support question, and it has a small number of causes — worth working through in this order, because it goes cheapest-first and each step rules out a whole class of problem. ## 1. Are postbacks arriving at all? Open [Conversions](https://app.postbackx.com/conversions) and look at the window where you expect them. **Nothing at all, ever.** The postback is not reaching PostbackX. Skip to step 2. **Some conversions, fewer than expected.** Postbacks work; something is wrong with specific ones. Skip to step 4. ## 2. Send one by hand This separates "your offer source is not calling us" from "the call is failing". Take a real click id from your [Clicks report](https://app.postbackx.com/clicks) and fire a postback yourself: ```bash curl -i "https://postbacks.postbackx.com/postback?click_id=REAL_CLICK_ID&payout=1.00&event_name=lead&transaction_id=manual-test-1" ``` **A `200` with a `conversion_id`.** PostbackX is working and the problem is on your offer source's side — its postback URL is wrong, or it is not firing. Go to step 3. **A `400`.** The click id was empty. Check for a stray space or an unsubstituted macro in what you sent. **A `404`.** That click id does not exist. Copy it again from the Clicks report. **A `410`.** The click is over 90 days old. Use a recent one. ## 3. Check the postback URL in your offer source Two things go wrong here, and they look identical from the outside. **The URL is wrong.** Compare it character by character against what [Offer Source](https://app.postbackx.com/offer-source) shows for that source. A URL copied out of a tutorial or an old email is the usual culprit. **The click id macro is wrong.** This is the more common one. Your offer source substitutes its own macros — if it stored your click id in `sub5`, the postback must send `click_id={sub5}`. Pointing it at `{sub1}` when the value is in `sub5` sends an empty click id, and PostbackX answers `400`. So check where the click id actually lands. Look at your offer URL in [Create Offers](https://app.postbackx.com/create-offers): whichever parameter carries `{click_id}` there is the one the postback must read back. ## 4. Check the click id is reaching the offer If postbacks arrive but many carry no click id, the chain broke a step earlier: the click id is not getting *into* the offer URL, so your offer source has nothing to give back. Your offer URL needs the macro. Not this: ``` https://track.yoursource.com/click?offer=123 ``` But this: ``` https://track.yoursource.com/click?offer=123&sub5={click_id} ``` PostbackX substitutes `{click_id}` at redirect time. Without it, every conversion arrives orphaned no matter how well the postback is configured. The quickest check: click your own tracking link and look at the URL you land on. If the click id is not visible in it, the macro is missing. ## 5. Conversions counted once that should be twice Two genuine conversions on the same click, same event, collapse into one when no `transaction_id` is sent — PostbackX cannot distinguish them from one conversion delivered twice. Add `transaction_id={transaction_id}` to your postback URL, using your offer source's own macro. Repeat deliveries then dedupe correctly and genuine repeats stay separate. See [attribution](/concepts/attribution). ## 6. Timing Two effects that look like missing data but are not. **Conversions lag clicks.** Someone clicks today and converts tomorrow. The conversion is attributed to the *click*, so it appears in the report for the day of the click, not the day of the sale. A report on today's traffic will keep filling in for days. **Time zones.** Your offer source reports in its zone, PostbackX in the one you select. A day boundary shifted by a few hours moves conversions between days and looks like a shortfall at the edges. Compare a whole week before concluding anything from a single day. ## Still missing Collect these before asking for help, since they are the first things anyone will want: - One click id you expect a conversion for - The exact postback URL configured in your offer source, macros included - What the `curl` in step 2 returned - The window and time zone you are comparing --- # Sending conversions back to ad platforms _How pixels return your conversion data to Facebook and the other platforms, so their algorithms optimize toward buyers._ Source: https://docs.postbackx.com/guides/pixels A pixel is how conversions get *out* of PostbackX and back to the ad platform. It is the opposite direction from a [postback](/reference/postback), and it is the step that changes what the ad platform's algorithm does. ## Why it matters more than it sounds An ad platform optimizes toward whatever outcome you report. Report nothing and it optimizes for clicks, because clicks are all it can see — and the cheapest clicks come from people who click a lot and buy nothing. Send conversions back and it can optimize toward people who actually convert. Same budget, different audience. This is usually the largest single improvement available to a new account, which is why it is worth doing before scaling spend. ## Add one Go to [Pixels](https://app.postbackx.com/pixels) and add a pixel for each platform you buy traffic on. The page has a **How it works** walkthrough. For **Facebook** you need two values, both from Events Manager: - The **Pixel ID** - A **Conversions API access token** Other platforms differ in detail; the page asks for what each one needs. Once a pixel exists, assign it to your campaigns. Conversions then flow automatically — there is nothing further to maintain. ## Server-side, not browser-side PostbackX sends conversions from its servers to the platform's API — for Facebook, the Conversions API, usually written CAPI. This matters because the browser is unreliable: ad blockers, tracking prevention and closed tabs all lose browser-fired events. A server-to-server event does not depend on the visitor's browser still being open, or cooperating. ## Match quality Facebook has to decide *which person* a conversion belongs to. The more identifying signal an event carries, the more often it succeeds, and the metric for this is Event Match Quality in Events Manager. Two things raise it: **The `_fbp` and `_fbc` cookies.** These exist only in the visitor's browser, so capturing them needs a page of yours with a [tracking snippet](/reference/snippets) on it. This is the main reason to use a [landing page](https://app.postbackx.com/landing-pages) on Facebook traffic rather than sending clicks straight to the offer. **Advertiser parameters.** `adv1` through `adv10` on a [postback](/reference/postback) are forwarded into the CAPI event. If your offer source can pass back hashed contact details, they go here. ## Check it arrived Events Manager shows received events within a few minutes. Fire a test conversion — the `curl` in [conversions are missing](/guides/missing-conversions) — and watch for it there. If the conversion is in PostbackX but not in Events Manager, the pixel is either not assigned to that campaign or its access token has expired. Tokens do expire, and the symptom is optimization quietly getting worse rather than an error, so it is worth checking when performance drifts for no other reason. --- # Custom tracking domains _Why to put tracking links on your own domain, and what to add to DNS._ Source: https://docs.postbackx.com/guides/tracking-domains A tracking domain is the hostname in your tracking links. By default PostbackX gives you a shared one; pointing your own at it produces links like `track.yourdomain.com` instead. ## Why bother **The link looks like yours.** A visitor hovering a link sees your brand rather than an unfamiliar host, and so does the ad reviewer. **You are not sharing reputation.** A shared domain's standing is affected by everyone using it. Your own domain answers only for your traffic. **You can move it.** A domain you control can be repointed. One you do not, cannot. ## Set one up [Domains](https://app.postbackx.com/domains) walks through it, with an animated demonstration for the common DNS providers — the wizard shows the actual screens for your registrar rather than describing them generically. The shape is always the same: add a subdomain you will use for tracking, create the DNS record PostbackX gives you, then wait for verification. DNS propagation is typically minutes and occasionally hours. The page shows verification state, so you do not have to guess. ## Then use it Set the new domain as default and new campaigns pick it up. > Existing campaigns keep the domain they were created with, and their live links > keep working. Changing the default does not rewrite links already behind running > ads — which is deliberate, since rewriting them would break every ad pointing at > the old host. To move an existing campaign, update it and replace the link in the ad. ## A note on the default domain The shared default works, and is fine while you are setting things up or testing. It is not what you want behind meaningful spend, for the reasons above. --- # Postback parameters _Every parameter the postback URL accepts, every response it returns, and what to paste into your offer source's postback field._ Source: https://docs.postbackx.com/reference/postback A postback is a URL your offer source calls when someone converts. It is the only way conversions get into PostbackX, and it needs no API key. ## The URL ``` https://postbacks.postbackx.com/postback?click_id={click_id}&payout={payout} ``` `GET` and `POST` both work. `POST` accepts either `application/json` or `application/x-www-form-urlencoded`, using the same parameter names. What you actually paste into your offer source depends on where it stored the click id. If you appended it as `sub5` on your offer URL, that is what its macro will be called: ``` https://postbacks.postbackx.com/postback?click_id={sub5}&payout={payout}&transaction_id={transaction_id} ``` The `{...}` parts are your offer source's own macros — it substitutes real values when it fires. Every platform names them differently, so copy them from its documentation, not from here. ## Parameters Only `click_id` is required. Every alias below is accepted as an equal alternative, because offer sources disagree about what these fields are called. | Parameter | Aliases | Required | Description | | --- | --- | --- | --- | | `click_id` | `aff_sub` | Yes | The click this conversion belongs to — the value PostbackX put into your offer URL. | | `payout` | `amount` | No | Revenue for this conversion, as a decimal. Defaults to `0`. | | `transaction_id` | `txn_id`, `order_id` | No | The offer source's own id for the conversion. Used to recognise repeat deliveries. | | `event_name` | `event` | No | Which conversion action fired, e.g. `lead` or `purchase`. Falls back to the campaign's default event. | | `sub1` … `sub5` | | No | Pass-through values, stored on the conversion for your own reporting. | | `adv1` … `adv10` | | No | Advertiser custom parameters, forwarded to Facebook's Conversions API to improve match quality. | > Any other parameter you add is stored on the conversion as well. Offer sources > each have their own extra fields, so rather than maintain a list of them, > PostbackX keeps whatever it is given. ## Responses The status codes are worth reading before you go looking for a bug, because two of them are deliberately not errors. | Status | Meaning | Body | | --- | --- | --- | | `200` | Recorded. | `success: true` with `conversion_id`, `click_id`, `processing_time_ms` | | `200` | Already recorded — a duplicate delivery. | `success: true`, `duplicate: true` | | `202` | Accepted, queued, and will be processed shortly. | `success: true`, `accepted: true` | | `400` | No `click_id`, or a body that could not be parsed. | `success: false` with `error` | | `404` | That click id does not exist. | `success: false` | | `410` | The click is more than 90 days old. | `success: false` | | `500` | Something failed on our side. | `success: false` | ### Why duplicates and outages return 2xx Most offer sources treat any non-2xx response as a failure and retry — some for days. That behavior is useful when a postback genuinely did not arrive, and harmful when it did. So a duplicate returns `200`: the conversion is already recorded, the retry has served its purpose, and there is nothing to gain from another attempt. And if the database is briefly unreachable, the postback is queued and answered `202` rather than failing — the conversion is safe, and telling the offer source otherwise would produce a retry storm on top of an outage. Treat `200` and `202` alike: the conversion is accounted for. ## Duplicate detection A conversion is considered a duplicate when its **click id, transaction id and event name** all match one already recorded. This is why `transaction_id` is worth sending even though it is optional. With it, repeat deliveries of the same conversion collapse into one, and two genuinely different sales on the same click stay separate. Without it, PostbackX cannot tell those two situations apart, and counts them as one conversion. The event name being part of the key is what lets one click produce both a lead and a later purchase. ## Testing it Fire a postback by hand, substituting a real click id from your [Clicks report](https://app.postbackx.com/clicks): ```bash curl -i "https://postbacks.postbackx.com/postback?click_id=REAL_CLICK_ID&payout=18.00&event_name=lead&transaction_id=manual-test-1" ``` A `200` with a `conversion_id` means the whole chain works. Run it again and you should get `duplicate: true` — proof that deduplication is doing its job. If you get `400`, the click id was empty: check that your offer source's macro matches the parameter you are sending it in. If you get `404`, the id does not exist — a typo, or a click from a different organization. For what to do when postbacks are not arriving at all, see [Conversions are missing](/guides/missing-conversions). --- # Tracking snippets _The three JavaScript snippets for landing pages — cookie enrichment, direct-link click creation, and firing a conversion from a thank-you page._ Source: https://docs.postbackx.com/reference/snippets Three scripts, for three different jobs on your own pages. You get the exact snippet for your account, with ids filled in, from [Direct Links](https://app.postbackx.com/direct-links) and [Landing Pages](https://app.postbackx.com/landing-pages) in the app — this page is the reference for what each option does. > The JavaScript objects are named `PropelDirect` and `PropelConvert`, and the HTML > attributes are `data-propel-*`. Those names date from the platform's original > working title. They are part of the public interface of code already running on > live landing pages, so they cannot be renamed without breaking those pages. Type > them exactly as shown. ## Which one you need | You want to | Use | | --- | --- | | Improve Facebook match quality on a landing page that PostbackX redirects to | `Propel` — [cookie enrichment](#cookie-enrichment) | | Point your ad straight at your own page, with no redirect | `PropelDirect` — [direct links](#direct-links) | | Fire a conversion from your own thank-you page | `PropelConvert` — [conversion firing](#conversion-firing) | The first two are alternatives to each other. The third is independent, and only needed when your offer source is not sending postbacks for you. ## Cookie enrichment For the normal flow, where your ad points at a PostbackX tracking link that redirects to your landing page. Facebook writes two cookies in the visitor's browser, `_fbp` and `_fbc`, that can only be read client-side. They materially improve Facebook's ability to match a conversion to a person, so this script reads them and attaches them to the click record. ```html ``` | Option | Default | Description | | --- | --- | --- | | `landerId` | | The landing page's id in PostbackX. | | `clickIdParam` | `click_id` | The URL parameter the click id arrives in. | | `apiUrl` | current origin | Where to send the enrichment call. | | `delay` | `2500` | Milliseconds to wait for the Facebook pixel to load before reading its cookies. | The delay matters: read the cookies too early and the pixel has not written them yet. If your pixel loads slowly, raise it. ### Without writing any JavaScript Put the attributes on any element instead, and the script initializes itself: ```html
``` ## Direct links For when your ad points **directly** at your own landing page. There is no redirect, so nothing has created a click record yet — this script creates it from the page itself. The trade-off: you keep full control of the URL in your ad and drop a redirect hop, but click recording now depends on the visitor's browser running your script. ```html ``` | Option | Required | Default | Description | | --- | --- | --- | --- | | `trackingId` | Yes | | Your PostbackX campaign's tracking id. | | `offerId` | Yes, unless `landerId` | | The offer to send traffic to. | | `apiUrl` | Yes | | The lander API base URL. | | `landerId` | No | | Use a landing page's weighted link routing instead of one fixed offer. Satisfies the `offerId` requirement. | | `delay` | No | `2500` | Milliseconds to wait for the Facebook pixel before reading its cookies. | | `cookieDomain` | No | current domain | Domain for the click id cookie. Set this to share a click across subdomains. | | `cookieExpiry` | No | `30` | Days to keep the click id cookie. | | `everflow` | No | | Everflow SDK integration; see below. | ### When it creates a click, and when it reuses one In order, stopping at the first that applies: 1. **A `click_id` is in the page URL.** The visitor arrived through a redirect that already created the click. It is reused, not duplicated. 2. **A `_propel_click_id` cookie exists** and the visitor's attribution has not changed. The same click is reused — a returning visitor is not a new click. 3. **Otherwise**, a new click record is created. "Attribution has not changed" means the ad parameters in the URL are the same. A visitor who comes back through a *different* ad gets a new click, because they are genuinely new traffic. ### Outbound links Any anchor carrying `data-propel-link` is rewritten to a tracked redirect, with the click id attached: ```html See your quote ``` Rewritten anchors are marked `data-propel-resolved="true"`. Links added to the page later are picked up automatically, so this works with content rendered after load. The script also injects the click id into forms on the page, so a form submission carries it to your backend. ### Everflow If you use Everflow's own SDK alongside PostbackX, pass its ids and both are fired together: ```javascript PropelDirect.init({ trackingId: 'YOUR_CAMPAIGN_TRACKING_ID', offerId: 'YOUR_OFFER_ID', apiUrl: 'https://propel-lander-api.propelsys.workers.dev', everflow: { offer_id: 123, affiliate_id: 456 } }) ``` `offer_id` and `affiliate_id` are required when the `everflow` block is present. `uid`, and `sub1` through `sub4`, are optional overrides. > The PostbackX click id is passed to Everflow as **`sub5`**. When you configure > Everflow's postback, its click id macro is therefore `{sub5}` — see > [postback parameters](/reference/postback). ### What it stores in the browser | Name | Where | Purpose | | --- | --- | --- | | `_propel_click_id` | Cookie and `localStorage` | The current click id. | | `_propel_attribution_key` | Cookie | Detects when a returning visitor arrived through a different ad. | ## Conversion firing For when the conversion happens on **your** page rather than the offer's, and no offer source is going to send a postback for it. Add this to the thank-you or confirmation page — the page a visitor only reaches after converting: ```html ``` | Option | Required | Default | Description | | --- | --- | --- | --- | | `postbackUrl` | Yes | | The postback host, **base URL only**. | | `payout` | No | `0` | Revenue for this conversion. `0` uses the offer's default payout. | | `eventName` | No | `lead` | Which conversion action fired. | | `clickId` | No | | Override the click id. Normally omit it and let the script find it. | > `postbackUrl` takes the host with no path: the script appends `/postback` > itself. Passing `https://postbacks.postbackx.com/postback` produces a doubled > path and the conversion is lost. Left to itself, the script finds the click id in this order: the `clickId` you passed, then the `_propel_click_id` cookie, then `localStorage`, then a `click_id` parameter in the page URL. Because this fires from a browser, treat it as best-effort: a visitor who closes the tab early never runs it. Where your offer source can send a real [postback](/reference/postback), prefer that. --- # REST API _Read your reporting data over HTTPS — authentication, conventions, endpoints and error handling._ Source: https://docs.postbackx.com/reference/api The API returns your own campaign data as JSON, from the same queries that back the dashboard — so the API and the UI always agree. The authoritative, machine-readable definition is the [OpenAPI specification](https://api.postbackx.com/v1/openapi.json). Most HTTP clients and code generators can consume it directly. This page is the orientation. ``` https://api.postbackx.com/v1 ``` ## Authentication Create a key on [API & MCP](https://app.postbackx.com/developers), then send it as a bearer token: ```bash curl -s "https://api.postbackx.com/v1/reports/totals?start_date=2026-07-01&end_date=2026-07-20" \ -H "Authorization: Bearer pbx_live_YOUR_KEY" ``` Keys are read-only and bound to a single organization. If your account belongs to several, a key reads the one it was issued for; to read another, issue a key there. Keep keys on a server or in a scheduled job. Never in browser code, and never in a committed file. ## Conventions Worth reading once — they explain most first-attempt failures. - **Dates are required.** Every reporting endpoint needs an explicit `start_date` and `end_date`. There is no default window. - **Ranges are inclusive** on both ends, and formatted `YYYY-MM-DD`. - **Time zones are yours to choose.** Pass an IANA name as `timezone`; it defaults to UTC, which is probably not how you read your own reports. - **Lists are comma-separated strings**, e.g. `campaign_ids=9c1f...,4ab2...`. - **Payloads are wrapped** in `data`, with context under `meta`. - **Metrics are pre-computed.** `roas`, `conversion_rate`, `epc` and `cpa` come back calculated. Recomputing them from the raw fields is how a report starts disagreeing with the dashboard. - **Rate limit is 1000 requests per hour** per credential. ## What is there **Reporting** — `/reports/totals` for one headline row, `/reports/timeseries` for a trend, `/reports/campaigns` and `/reports/offers` for per-entity rows, `/reports/breakdown` for the ad-platform hierarchy down to the creative, and `/reports/calls` for inbound call attribution. **Entities** — `/campaigns`, `/offers`, `/offer-sources`, `/tracking-domains`, `/organizations`. Use these to turn the ids that reporting returns into names. **Raw data** — `/clicks` and `/conversions`, one row per record. These are the heavy ones: page through them and store the results rather than re-pulling the same window. **Writes** — `PATCH /campaigns/{id}` and `PATCH /offers/{id}`, accepting `status` only (`active`, `paused` or `archived`). That is deliberately the entire write surface, so an automation can pause a loser without any risk of rewriting its configuration. > Conversions cannot be created through this API. They arrive at the > [postback URL](/reference/postback), which also handles deduplication, click > matching and delivery to the ad platforms — a row written directly would reach none > of those. The full parameter list for each endpoint is in the [specification](https://api.postbackx.com/v1/openapi.json) and in the searchable explorer on [API & MCP](https://app.postbackx.com/developers). ## Reading the breakdown endpoint `/reports/breakdown` is the one with a shape worth explaining, because it returns the whole tree at once rather than one level per call. Every row carries its own `level`, `entity_key` and `parent_key`. You rebuild the hierarchy by matching a row's `parent_key` to the `entity_key` above it. Pass `level` to get a single rung instead of everything. Level names are platform-specific, because each platform names its own hierarchy (`adset` on Facebook, `tt_adgroup` on TikTok). The cross-platform shorthands `campaign`, `adgroup` and `creative` fan out across all of them, and are what you want unless you are targeting one platform. ## Errors Every failure uses one envelope: ```json { "error": { "type": "validation_failed", "message": "One or more parameters were rejected.", "fields": [ { "field": "start_date", "code": "required", "message": "start_date is required." } ] } } ``` **Branch on `error.type`, never on the message.** Messages are written for people and will be reworded; the types are the contract. | Type | Status | What to do | | --- | --- | --- | | `validation_failed` | 422 | Read `error.fields`; each names the parameter and why. | | `unauthorized` | 401 | The key is missing, malformed or revoked. | | `forbidden` | 403 | Valid key, wrong organization. | | `not_found` | 404 | No such endpoint or record. | | `rate_limited` | 429 | Back off for `error.retry_after_seconds`. | | `internal_error` | 500, 502 | Our side. Retry with exponential backoff. | ## Not writing code If the goal is answers rather than a integration, connect an [AI assistant](/reference/mcp) instead — same data, no client to maintain. The app's [API & MCP](https://app.postbackx.com/developers) page also has ready-made snippets for Google Sheets, Python, Make and Zapier. --- # Connecting an AI assistant _Connect Claude or ChatGPT to your PostbackX account over MCP, and ask about your campaigns in plain language._ Source: https://docs.postbackx.com/reference/mcp PostbackX runs an MCP server, which lets an AI assistant read your reporting data directly. Connect it once and you can ask which campaigns lost money last week instead of building the report yourself. Access is read-only and scoped to one organization, the same as an API key. ## The endpoint ``` https://mcp.postbackx.com/mcp ``` ## Claude Code ```bash claude mcp add --transport http postbackx https://mcp.postbackx.com/mcp ``` Then run `/mcp` inside Claude and choose **Authenticate** to sign in. You can also pass a key directly with `--header "Authorization: Bearer pbx_live_..."`, but signing in avoids putting a credential in your shell history. ## Claude Desktop, claude.ai and ChatGPT Add it as a custom connector, using the same URL: - **Claude Desktop or claude.ai** — Settings, then Connectors, then Add custom connector. - **ChatGPT** — Settings, Security and login, enable Developer mode, then add the same URL. Full setup with animated walkthroughs is on [API & MCP](https://app.postbackx.com/developers) in the app, which also mints the API keys. ## What it can do Reporting: totals, time series, and breakdowns by campaign, offer, ad and inbound call. Listing: campaigns, offers, offer sources, tracking domains, organizations, and raw click and conversion rows. It can also draft a campaign link for you — which produces a pre-filled URL into the app for you to review, not a created campaign. Status changes to campaigns and offers exist as tools but are **disabled by default**, so a connected assistant cannot pause anything unless writes are explicitly turned on for your deployment. ## Asking useful questions **Always say what period you mean.** There is no default window; an assistant will either ask or guess, and guessing is worse. **Ask for the comparison you want, not the rows.** "Which offers beat 1.2 ROAS last week, and what did each spend?" gets you an answer. "Show me my conversions" gets you a wall of rows. **Metrics are already computed.** ROAS, EPC and CPA come back calculated — an assistant that recomputes them from raw fields can drift from what the dashboard shows. **Watch the vocabulary.** An offer source is where offers come from; a platform is where ad spend goes. Assistants trained on the wider industry tend to say "network" for the first. See [vocabulary](/concepts/vocabulary). ## If you would rather write code The [REST API](/reference/api) is the same data over plain HTTP, with an [OpenAPI specification](https://api.postbackx.com/v1/openapi.json) that most frameworks can generate a client from.