
Publisher docs
Integrate your app with 
The same integration guide publishers see inside the dashboard.
Troubleshooting
What goes wrong most often, what fixes it, and the questions we get asked.
Common errors
What goes wrong most often, and what fixes it.
| Error / behaviour | Cause | Fix |
|---|---|---|
| "This content is blocked. Contact the site owner" where the wall should be | Your page's Content Security Policy doesn't allow framing loothq.com. The browser blocks the iframe before it loads, so this never reaches us. | Add https://www.loothq.com to your frame-src directive (see the iFrame section). |
| Script embed: no wall appears, and no LootHQ request is made | Your page's Content Security Policy doesn't allow loading scripts from loothq.com. The browser refuses to fetch wall.js before anything runs, so this never reaches us; the signature is a script-src CSP violation in the browser console. | Add https://www.loothq.com to your script-src directive as well as frame-src (see the Script section). |
| Wall shows "Missing placement_id" | You didn't pass a placement ID, or it's invalid / inactive. | Add ?placement_id=YOUR_PLACEMENT_ID. Get the ID from dashboard → Placements. |
| Users don't get rewards | Postback not configured or returning non-200. | Set your postback URL in Settings and ensure it returns 200 OK. |
Every attempt fails with Redirect (30x) to … — it points at a different host (or it downgrades to plain http, it exceeds the 2-hop redirect limit) | We do follow redirects on postback deliveries, but only up to 2 hops, HTTPS-only, and only to the same host or its www/apex variant. This error means the redirect left that envelope: a different hostname (a CDN or a link shortener), plain http, or a 3rd hop. A plain apex⇄www redirect is followed and never produces it. | Save the FINAL address as your postback URL (the delivery row prints it), then confirm with Test delivery on the Settings page. |
| Deliveries fail with a TLS or certificate error | Expired certificate, missing intermediate chain, or a certificate that doesn't cover the hostname you saved (very common on the apex when only www is covered). | Fix the chain, or save the hostname the certificate actually covers. Test delivery reports the exact TLS reason. |
| Deliveries retried although your endpoint is up; every attempt shows a timeout | Each attempt has a 10-second budget, redirect hops included. A handler that credits synchronously against a slow database, or calls a third-party API before answering, blows it, and the delivery is retried, so your slow path runs again. | Answer 200 first and do slow work after responding (queue it). The retry schedule is in Delivery and retries. |
Every delivery to your endpoint fails with 401 / 403 | Your own auth is rejecting us: usually the key=… secret is missing from the saved template (it is a literal param, not a macro, so it is easy to lose when editing the URL), still the literal YOUR_SECRET placeholder from the default, or your endpoint expects a header or IP allowlist we cannot satisfy. | Re-save the template with your key= param intact and set to your real token (not YOUR_SECRET), and auth on that alone. Postbacks carry no custom headers, and come from dynamic IPs unless we have put your account on a fixed relay address. Confirm with Test delivery. |
Your call to /api/v1/offers answers 401 or 403 | 401: invalid or missing API key. 403: your account is not active, API access is not enabled on it, the placement is inactive, or the placement is a Hosted Wall one, which the feed refuses by design. | Send Authorization: Bearer <API_KEY> with the key from the Offers API page, and use a placement whose integration type is Offers API. If the account itself is blocked, the error body says to contact support. |
| Feed works, clicks flow, but nothing ever credits (Offers API) | You are calling the feed without user_id/sub_id, so the tracking URLs carry no sub-id. Depending on your account's rollout state, such clicks are either not recorded at all ("grace") or refused outright ("enforced"). Either way they can never be credited to anyone. The response told you: its warning and sub_id_enforcement fields are non-null. | Pass user_id=<your end-user id> on every feed call and assert warning === null in your own build. See the Offers API quickstart. |
| Duplicate rewards | Postbacks re-processed without checking the ({transaction_id}, {event_id}) pair. Retries and manual Resends reuse both values verbatim, so an unchecked handler credits every repeat. | Store each pair; return 200 for repeats without re-crediting. See Dedupe keys per channel. |
| Milestone offers only ever credit their first goal | You are deduping on {transaction_id} alone, which on every conversion postback is the click id. Every goal of a multi-goal offer repeats it, so your handler answers 200 already processed and discards goals 2..n. No error is logged on either side. | Dedupe on the ({transaction_id}, {event_id}) pair: {event_id} carries the goal id there, so the pair is unique per credit. (The conversion.approved webhook's conversionId is a signed alternative.) See Dedupe keys per channel. |
| Your reversal handling never fires, and nothing looks wrong | You are waiting for a negative {payout} or status=reversed on the primary postback URL. Neither is ever sent there. Approvals keep arriving normally, so the integration looks healthy while revoking nothing. | Set a reversal postback URL in Settings (or subscribe to conversion.reversed on the Webhooks page), then prove it with the tester's Reversal mode. See Handling reversals. |
{offer_id} is empty on a postback | It is a prize redemption: {source} reads prize, and no single offer produced the credit, so there is no offer id to send. Every conversion postback, hosted wall and Offers API alike, carries {offer_id}. | Nothing to fix. Branch on source === "prize" if you want to label these separately; per-offer reporting is unaffected. |
| Users keep clawed-back rewards; your reversal ledger fills up but your code never hears about it | No reversal postback URL is saved. Reversals never arrive on the primary URL, so with no reversal URL (and no webhook) they are settled on our side only. Settings warns about exactly this when you save a primary URL alone. | Save a reversal postback URL in Settings (the same endpoint is fine; see the single-endpoint pattern in Handling reversals), then prove it with the tester's Reversal mode. |
Empty values, or literal {braces}, in your postback params | Two distinct signatures. A param carrying the literal macro text (e.g. payout={payoutt}) means the macro name is misspelled: we only substitute names we recognize and pass anything else through untouched. A param that is present but empty is usually normal: that macro simply is not supplied on this channel (see the per-channel matrix in Postbacks). | For braces: fix the spelling against the macro table. For empties: check the matrix before treating it as a bug, and never reject a request because an inapplicable macro is empty. Use the Postback Tester to confirm. |
FAQ
Can I use multiple apps / sites under one publisher account?
Yes. You can create multiple placements for different apps, sites, and GEO splits under a single publisher account.
How often are stats updated in the dashboard?
Clicks appear straight away. Conversions appear as soon as the upstream network reports them, which is seconds on some networks and minutes on others. Balances and wallet figures move as conversions are processed.
What should I use as the user / subID?
Use a stable, unique identifier for each user (internal user ID, UUID, or wallet address). Do not use sensitive data like emails in plain text. It must be unique across ALL your placements, not just one. Reusing a plain per-app numeric ID across multiple apps or sites will merge those users' coin balances and history. Namespace it per placement if there's any chance of collision.
Can I disable certain GEOs or offer types?
Yes. Reach out to support or your LootHQ rep to adjust GEOs, categories, or traffic restrictions on your account.
Can I whitelist your postback IPs?
Not by default. Postbacks fire from cloud infrastructure with dynamic IPs, so there is no standing list to publish. If your edge runs a default-deny filter and you cannot add us without an address, ask your LootHQ contact: we can move your account onto a fixed relay address and give you the value to allowlist. It is set up per publisher on request, so tell us before you build against it.
Even then, do not make the IP your only check. A source address proves where a request came from, not who sent it. Authenticate with a secret token in your postback URL. It travels over TLS, proves the sender holds your secret, and you can rotate it yourself any time by editing the URL. Combined with idempotent {transaction_id} handling, replayed or forged postbacks credit nothing. If you want cryptographic verification on top, subscribe on the Webhooks page. Every delivery is HMAC-signed and timestamped (x-loothq-signature), the same model Stripe and GitHub use.
A user says their offer didn't credit. Who handles it?
LootHQ does. The coin ledger is ours, not yours, so credit disputes don't route through your support queue. Users raise them directly in the wall, where the support entry point links the report straight to their click, so our team can check it against the original postback receipt. You don't need to build anything for this; just point confused users back to the wall.
Do you have a leaderboard?
Optionally, per placement: a single network-wide top-25, monthly season board. Every user sees their own rank and distance to #25 even when they're not in the 25. Login is optional and only prompted after a user's first conversion; signing in counts their prior earnings retroactively (nothing is lost) and clears their pending coins faster. Anonymous sub_id earning is unaffected, and no publisher attribution is ever shown. It has no effect on your integration; full leaderboard docs are separate.