Developers · Referral webhooks
Report conversions from creator referral codes
When someone signs up, installs, deposits or buys with a creator’s code, your server sends Easily Promote one signed HTTPS request. We match the code to the creator and credit them. No SDK, nothing in your sign-up flow, and no personal data.
- Method
- POST, JSON body
- Authentication
- Key ID + HMAC-SHA256
- Test mode
- "test": true
How it works
Easily Promote
A creator joins your campaign and gets a code
Each creator gets one code, like ACME-TUNDE. You can also supply codes from your own referral system instead.
Creator
The creator shares the code with their audience
Viewers see the content and enter the code when they sign up, install, deposit or buy. They never interact with Easily Promote.
Your server
Your app checks the code with us
When a user enters a code, your server asks us whether it’s valid and accepts or rejects it. There is nothing to load or sync — a new creator’s code works the moment they join.
Your server
Your server tells us about the conversion
When your definition of a conversion happens, send one signed POST with the code, the event type and a unique event ID.
Easily Promote
We credit the creator
We verify the signature, match the code to its creator and update your campaign and the creator’s dashboard in real time.
Referral tracking runs alongside a campaign’s views. Creators can start posting before your integration is live; conversions start counting as soon as you send them.
Quickstart
- In the Easily Promote app, open Referral tracking settings and generate a signing key.
- Store the key ID and secret on your server as EP_KEY_ID and EP_WEBHOOK_SECRET. Never ship them in an app or website.
- Add a referral code field to your sign-up (or checkout) and check each code with the validation endpoint before accepting it.
- Send a conversion every time someone who used a valid code converts. Use "test": true while you build — nothing is counted.
- Turn on referral tracking for a campaign. Creators’ codes start working as soon as they join.
Signing requests
Every request carries two headers. X-EP-Key-Id says which key signed it. X-EP-Signature proves the body came from you and hasn’t been changed or replayed.
- Serialize the JSON body to a string. Sign and send that exact string — re-serializing after signing changes the bytes and the signature will not match.
- Take the current Unix time in seconds as
t. It must be within 5 minutes of our clock. - Compute HMAC-SHA256 of
${t}.${body}using your secret, as lowercase hex. - Send
X-EP-Signature: t=<t>,v1=<hex>.
Check your implementation
Sign these values with your code. If you get the same signature, your signing is correct. This secret is an example only and does not work against the API.
- Secret
- whsec_example_do_not_use_4f9a2c81d0e3
- t
- 1789408800
- Body
- {"event_id":"signup-10482","code":"ACME-TUNDE","event":"signup","timestamp":"2026-09-14T18:00:00Z"}
- String to sign
- 1789408800.{"event_id":"signup-10482","code":"ACME-TUNDE","event":"signup","timestamp":"2026-09-14T18:00:00Z"}
- Expected v1
- d92d13806363429d72242ce2c94acbc5629dba5631e101f773842c9ea6016203
Checking a code
When a user enters a referral code in your sign-up, checkout or deposit flow, ask us whether it’s valid before you accept it. You never load or sync creators’ codes — a code works the moment its creator joins a campaign. The endpoint is https://api.easilypromote.com/api/webhooks/codes/validate.
POST https://api.easilypromote.com/api/webhooks/codes/validate
Content-Type: application/json
X-EP-Key-Id: key_4f9a2c81d0e3
X-EP-Signature: t=1789408800,v1=…
{ "code": "ACME-TUNDE" }Sign it exactly like a conversion. A code check records nothing; report the conversion separately once it happens.
| Status | Body | What to do |
|---|---|---|
200 | {"valid":true,"code":"ACME-TUNDE","campaign_id":"6aa8…","event":"signup"} | Accept the code and store it on the new user so you can report their conversion later. |
200 | {"valid":false,"code":"ACME-TUNDE","reason":"not_found"} | Tell the user the code isn’t recognised. The reasons are listed below. |
400 · 401 · 429 · 5xx | {"error":"…"} | Same meaning as for conversions. Don’t block the user — accept the code and let the conversion decide. |
not_found- No creator in your account has this code.
disabled- You turned this code off.
campaign_not_accepting- The code’s campaign was cancelled or ended more than 7 days ago.
Don’t block sign-ups on this call. Use a short timeout, around 3 seconds. If the check errors or times out, accept the code anyway and report the conversion as normal — the conversion webhook rejects codes that were never valid.
Reporting a conversion
Copy your exact URLs from Referral tracking settings in the app. In production the conversion endpoint is https://api.easilypromote.com/api/webhooks/conversions.
POST https://api.easilypromote.com/api/webhooks/conversions
Content-Type: application/json
X-EP-Key-Id: key_4f9a2c81d0e3
X-EP-Signature: t=1789408800,v1=d92d1380…6016203
{
"event_id": "signup-10482",
"code": "ACME-TUNDE",
"event": "signup",
"timestamp": "2026-09-14T18:00:00Z"
}| Field | Type | Required | Description |
|---|---|---|---|
event_id | string, 1–128 chars | Yes | Your own unique ID for this conversion, such as the new user’s internal ID or a signup record ID. Sending the same event_id again is safe and is never counted twice.e.g. "signup-10482" |
code | string, 1–64 chars | Yes | The referral code the user entered. Case-insensitive.e.g. "ACME-TUNDE" |
event | enum | Yes | One of install, signup, purchase, deposit or custom. Every event is stored, but only the event type chosen on the campaign counts toward its conversions.e.g. "signup" |
timestamp | ISO 8601 string | Yes | When the conversion happened, with a timezone offset or Z.e.g. "2026-09-14T18:00:00Z" |
test | boolean | No | Set true to verify your key and signature without recording anything. The code does not need to exist; the response tells you whether it would match.e.g. true |
Conversion responses
Every response is JSON. A 200 means we have the event — including duplicates — so you can stop sending it.
| Status | Body | Meaning | Retry? |
|---|---|---|---|
200 | {"status":"recorded","counted":true} | Saved and credited. counted is false when the event type differs from the campaign’s. | No |
200 | {"status":"ignored","reason":"event_id already recorded"} | A duplicate of an event you already sent. Treat as success. | No |
200 | {"status":"test_ok","code":{"value":"ACME-TUNDE","found":true,…}} | Test event verified. Nothing was recorded. | No |
400 | {"error":"Invalid payload","details":["event: …"]} | Missing or invalid field, or the body is not JSON. details says which field. | Fix the request |
401 | {"error":"Signature does not match"} | Missing, unknown, revoked or expired key; a signature that does not match; or a timestamp more than 5 minutes from our clock. | Fix the request |
404 | {"error":"Unknown referral code"} | No creator in your account has that code. | No |
409 | {"error":"Campaign is not accepting conversions"} | The code is disabled, or its campaign ended more than 7 days ago or was cancelled. | No |
413 | {"error":"…"} | Body larger than 16 KB. | Fix the request |
429 | {"error":"Too many requests for this key. Slow down and retry."} | More than 50 requests per second for one key. | Yes, with backoff |
5xx | — | Something went wrong on our side. | Yes, with backoff |
Retries and duplicates
Networks fail, so send events from a queue or background job rather than inside the user’s request.
- Retry timeouts, connection errors,
429and5xxwith exponential backoff — for example after 1s, 2s, 4s, 8s — for up to 24 hours. - Keep the same
event_idon every retry. If an earlier attempt did reach us, the retry returnsignoredand nothing is counted twice. - Re-sign each attempt with a fresh
t; a signature older than 5 minutes is rejected. - Don’t retry
400,401,404or409without changing something — the same request will fail the same way.
Code examples
const crypto = require("crypto");
// Call from your sign-up handler before accepting a referral code.
async function checkReferralCode(code) {
const body = JSON.stringify({ code });
const t = Math.floor(Date.now() / 1000);
const v1 = crypto
.createHmac("sha256", process.env.EP_WEBHOOK_SECRET)
.update(`${t}.${body}`)
.digest("hex");
const res = await fetch("https://api.easilypromote.com/api/webhooks/codes/validate", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-EP-Key-Id": process.env.EP_KEY_ID,
"X-EP-Signature": `t=${t},v1=${v1}`,
},
body,
signal: AbortSignal.timeout(3000),
});
if (!res.ok) throw new Error(`Code check failed: ${res.status}`);
const result = await res.json(); // { valid, code, reason? }
return result.valid;
}Store EP_KEY_ID and EP_WEBHOOK_SECRET as environment variables on your server. Both cURL examples are safe to run as-is: a code check records nothing, and the conversion example sends a test event.
Testing
Try requests from the app
Referral tracking settings can check a code or send a test conversion for you. We sign the request with your key and run it through the real checks, so you can see a working exchange before writing any code.
Send test events from your server
Add "test": true to any request. We verify the key and signature, mark you Connected and tell you whether the code would match — without recording a conversion.
Read the request log
Recent requests in the app lists every signed request from the last 30 days with the status we returned and the reason for any rejection.
Getting 401 “Signature does not match”? Check you signed the exact body string you sent, used the secret (not the key ID), and wrote t in seconds, not milliseconds. Compare against the test values under Signing requests.
FAQ
Do we need to load creators’ codes into our system?
No. Check each code with the validation endpoint when a user enters it. A new creator’s code works as soon as they join. If your system can only accept codes it already stores, download them as a CSV from the campaign’s Referrals tab instead.
What if the code check fails or times out?
Don’t block the user. Accept the code, store it, and report the conversion as normal — the conversion webhook answers 404 if the code was never valid, so nothing is credited by mistake.
Do creators need an account on our platform?
No. Codes move between you and Easily Promote only. Creators just share the code; your users enter it the way they normally would.
We already have our own referral codes. Can we use them?
Yes. Choose “We’ll use our own codes” on the campaign, then set a code per creator or import a CSV with creator_username and code columns. The webhook works the same way.
What personal data should we send?
None. Send only the code, the event type, your event ID and the time. Do not put names, emails or phone numbers in event_id either.
Which events count as conversions?
Each campaign picks one event type, such as signup. We store every event you send, but only that type moves the campaign’s conversion count.
What happens when a campaign ends?
Completed campaigns keep accepting conversions for 7 days so late events are not lost. After that, and for cancelled campaigns, you get a 409.
How do we rotate a key without downtime?
Click Rotate in the app. The old key keeps working for 24 hours while you deploy the new one, then stops. Revoke a key immediately if it leaks.
Can several servers send events?
Yes. You can have up to three active keys, for example one per service, and every server can send to the same URL.
Stuck? Email [email protected] with the event_id and the response you received.