Troubleshooting
Fix the common OpenPromoter problems: tracking not recording, sales not credited, webhook signature errors, failed payouts, partner sign-in, custom domains and email.
Use this page when something in your program doesn't work as expected. Each entry starts with what you see and follows with the likely cause and the fix. Messages in quotes are the exact words the app shows.
Tracking is not recording clicks
What you see: Settings > Install shows Not seen yet, or First click recorded stays unticked after you test a partner link.
Why: op.js isn't on the page, its data-program doesn't match your program, or an ad blocker stopped it. A consent banner can also hold it back with data-consent="required".
Fix:
- Open your site and view the page source. Check that the
op.jstag is in the<head>with your program slug. - Load the page once in a private window with ad blockers off. Then click Check again on Settings > Install.
- If ad blockers are common among your visitors, proxy op.js through your own domain with
data-api="/op". See Ad blockers. - With consent mode on, call
openpromoter("consent", true)after the visitor accepts. - Test with an approved partner's link. Links of pending, paused or banned partners don't record clicks.
op.js reports the install once per browser session. The status turns to Script seen on the first report.
A sale was not credited to the partner
What you see: the customer paid, but no commission appears under Commissions.
Why: the sale needs a signal: a coupon code, the click id, a partner code in metadata, the visitor's clicks or the lead email. If none of these reach OpenPromoter, nobody gets the sale. See Tracking for the full order.
Fix:
- On Settings > Install, check that Billing webhook received is ticked. If not, reconnect billing in Settings > Integrations.
- With your own Stripe Checkout, pass the click id as
client_reference_idand assubscription_data.metadata.op_click. Payment Links and the Pricing Table get it from op.js by default. - For Paddle, Lemon Squeezy or Polar, pass
op_clickas custom data. See Billing providers. - Check the purchase happened inside the cookie window (60 days by default) and in the same browser as the click.
- If you record signups, call
openpromoter.signup({ email })before payment. A later payment by that email is then credited.
A payment that arrives before its partner is known is kept for two days. If the lead or click arrives in that time, the commission is booked then.
On a self-hosted instance on localhost, Stripe can't reach you. Run stripe listen and paste the signing secret it prints. See Self-hosting.
Webhook signature checks fail
What you see: your endpoint rejects OpenPromoter's webhooks, or the SDK throws "Invalid signature" or "Signature timestamp outside tolerance".
Why: the signature covers the exact raw body. Parsing the JSON first, using another endpoint's secret or a server clock that is off all break the check.
Fix:
- Verify the raw body before you parse it. In Express use
express.raw({ type: "application/json" })on that route. In Next.js useawait req.text(). - Copy the signing secret shown under that endpoint in Settings > API & webhooks. Each endpoint has its own
whsec_…secret. - Sync your server clock. Signatures older than 5 minutes are refused.
- With
openpromoter listen, verify with the same--secretyou passed to the CLI.
Click Test on the endpoint to send a sample event after each change. See Webhooks.
Incoming billing webhooks fail the same way. If Stripe shows "Invalid signature" for its deliveries, paste the endpoint's current signing secret into Settings > Integrations.
A payout failed
What you see: a payout on the Payouts page has the Failed status with a reason beside it.
Why and fix:
| Reason shown | Fix |
|---|---|
| Partner has no PayPal email | Ask the partner to save their PayPal email under Payouts in their portal |
| Partner has no Wise email | Ask the partner to save their Wise email in their portal |
| Partner has not finished Stripe onboarding | Ask the partner to click Finish Stripe setup in their portal |
| PayPal does not pay out in this currency | Pay that partner with Wise or bank transfer |
| This partner needs to submit a W-9 (with SSN/EIN) | A US partner paid outside Stripe passed the IRS 1099 limit. They add a W-9 under Settings in their portal |
| Wise could not fund it | Add money to your Wise balance |
| stripe, paypal or wise is not connected | Connect that provider in Settings > Integrations |
After you fix the cause, click Retry on the payout. A failed payout releases its commissions to the partner's balance. They also go out in the next run.
A payout that shows "Checking with paypal" (or Wise) is still processing. The provider took the request and OpenPromoter checks the result later. Don't pay it again by hand.
If your Stripe account is registered in a country such as India, Stripe can't pay partners through Connect. Use Wise or bank transfer for those partners.
A partner can't sign in
What you see: a partner says sign-in fails or they land on the application form.
Why and fix:
- Wrong address. Partners sign in on your program's address: your verified custom domain or
<slug>.openpromoter.com. Send them the Partner login link from your program page. - Forgotten password. They click Forgot password? and then Send reset link. The new password needs at least 8 characters.
- Google or GitHub fails. If they see "This account uses two-factor authentication", they sign in with email and password instead. "That email already has an account" means they first sign in with their password.
- No partner record yet. A signed-in person who isn't a partner in that program is sent to the application form. Check Partners for their email.
- Account paused, rejected or banned. The portal shows "Your partner account is paused" (or the other status). Change their status on their partner page.
A custom domain stays pending
What you see: Settings > Portal & domain keeps showing Check DNS and HTTPS (or Verify DNS when self-hosting) instead of Verified.
Why and fix:
| Message | Fix |
|---|---|
Waiting for DNS. Add a CNAME from your domain to domains.openpromoter.com |
Add the CNAME record at your DNS provider. Use the exact Name and Value shown on the page |
| No CNAME to the target found for your domain yet | Self-hosting: the CNAME must point at your APP_URL host or CUSTOM_DOMAIN_TARGET |
| DNS is set. Cloudflare is issuing the certificate | Wait a few minutes and check again |
| This domain isn't registered yet. Save it again | Click Save next to the domain once more |
| Enter a domain like partners.yourapp.com | Use a subdomain you own. A CNAME can't sit on a root domain such as yourapp.com |
| Another program already uses this domain | Each domain serves one program. Pick another subdomain |
DNS changes can take a few minutes to reach everyone. See Custom domains.
Emails are not arriving
What you see: partners or teammates don't get invitations, approvals or password reset emails.
Why and fix:
- Ask them to check their spam folder and to add your sending address to their contacts.
- Check the email address. Addresses ending in
.test,.example,.invalidor.localhostnever receive mail. The demo and test data use them on purpose. - Self-hosting: set
SMTP_URLorRESEND_API_KEY. Without either, emails are only logged and never sent. SetEMAIL_FROMto an address your mail provider has verified. - With
docker compose, every email lands in Mailpit athttp://localhost:8025instead of a real inbox.
The Stripe key is refused
What you see: connecting a restricted key in Settings > Integrations fails, or the webhook isn't registered.
Why and fix:
- "That doesn't look like a Stripe secret or restricted key." Paste a key that starts with
rk_live_,rk_test_,sk_live_orsk_test_. - A permission error from Stripe. The restricted key is missing a permission. Add the ones in the Stripe permissions table and create the key again.
- The webhook isn't registered. The key needs Webhook Endpoints with Write access. Or add the endpoint in Stripe yourself and paste its signing secret.
- Partner payouts through Stripe fail. Add Accounts, Account Links and Transfers with Write access.
The API returns an error
| Status | Fix |
|---|---|
401 unauthorized |
Send Authorization: Bearer <key>. Check that the key wasn't revoked |
403 forbidden |
The message names the scope the key needs. Create a key with that scope |
404 not_found |
Check the program id and that the key belongs to the same workspace |
429 rate_limited |
Each key allows 600 requests a minute. Back off and retry |
See the API reference for every code.
Next: Quick start