Skip to content

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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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

  1. In the Easily Promote app, open Referral tracking settings and generate a signing key.
  2. 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.
  3. Add a referral code field to your sign-up (or checkout) and check each code with the validation endpoint before accepting it.
  4. Send a conversion every time someone who used a valid code converts. Use "test": true while you build — nothing is counted.
  5. 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.

  1. 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.
  2. Take the current Unix time in seconds as t. It must be within 5 minutes of our clock.
  3. Compute HMAC-SHA256 of ${t}.${body} using your secret, as lowercase hex.
  4. 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.

HTTP
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.

Code check responses
StatusBodyWhat 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.

HTTP
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"
}
Request body fields
FieldTypeRequiredDescription
event_idstring, 1–128 charsYesYour 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"
codestring, 1–64 charsYesThe referral code the user entered. Case-insensitive.e.g. "ACME-TUNDE"
eventenumYesOne 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"
timestampISO 8601 stringYesWhen the conversion happened, with a timezone offset or Z.e.g. "2026-09-14T18:00:00Z"
testbooleanNoSet 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.

Webhook responses
StatusBodyMeaningRetry?
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, 429 and 5xx with exponential backoff — for example after 1s, 2s, 4s, 8s — for up to 24 hours.
  • Keep the same event_id on every retry. If an earlier attempt did reach us, the retry returns ignored and 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, 404 or 409 without changing something — the same request will fail the same way.

Code examples

Node.js · Check a code
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.