Integration Spec

Zoul → Central Affiliate Platform — Integration Spec (v1.2 FINAL)

Zoul sends per-user events only. All eligibility, Net Growth and commission math runs centrally on the affiliate platform. Zoul never sends counts, totals or amounts.


1. Affiliate links (AppsFlyer OneLink)

Base OneLink (production, no affiliate code embedded):

https://zoul.onelink.me/X33D/de7q9ov9

Dynamic parameter: ref={AFFILIATE_CODE}

Final affiliate URL — the affiliate platform appends the ambassador's real code:

https://zoul.onelink.me/X33D/de7q9ov9?ref={AFFILIATE_CODE}

Example:

https://zoul.onelink.me/X33D/de7q9ov9?ref=YUI

The parameter is exactly ref. The code always comes from the affiliate system — never hardcoded. The Affiliate Dashboard Copy / Share action for Zoul produces exactly this URL.

Delivery of ref (confirmed by Zoul Dev)

ScenarioMechanismref received
App already installedAppsFlyer Deep LinkYes
New installAppsFlyer Deferred Deep Link / First OpenYes

Zoul stores the received ref value against the user and sends that value to Central as affiliate_code in the user events below. ref is customer-facing only — the API contract field name stays affiliate_code.


2. Endpoint

POST https://lescal.com/api/apps/6a06fc7184e0c5172c32d204/functions/zoulUserEvent
Content-Type: application/json

Headers (all required)

HeaderValue
x-api-keyZoul API key (issued separately, out of band)
x-timestampUnix seconds, must be within 5 minutes of server time
x-signatureHMAC_SHA256(api_key, "<timestamp>.<raw json body>"), hex

The signature is computed over the exact raw body string that is sent.


3. Events

EventWhen to sendEffect
UserVerifieduser completes verification / qualification (once per user)marks user verified (never reset)
UserActivityqualifying session / activityupdates last_activity_at
AccountDeletedaccount deleted / closeduser excluded from eligibility

Body

{
  "website": "zoul.app",
  "event": "UserVerified",
  "conversion_id": "zoul_evt_000123",
  "external_user_id": "zoul_user_4821",
  "affiliate_code": "YUI",
  "event_at": "2026-08-13T08:00:00Z"
}
FieldRequiredNotes
websiteyesalways zoul.app
eventyesone of the three above
conversion_idyesunique per event; resending the same id is ignored (idempotent)
external_user_idyespermanent, stable user id — same across devices and sessions
affiliate_codeon first eventthe value received from ref in the OneLink
event_atrecommendedISO 8601 UTC; defaults to receipt time

UserActivity should be sent at most once per user per day.


4. Attribution rules

  • The affiliate is locked on the first event carrying a code and is never changed.
  • A later event with a different code is accepted, the user keeps the original affiliate, and an attribution-conflict counter is incremented for audit.
  • Events without a code for an already-known user are fine.
  • Attribution is keyed on external_user_id, so a reinstall or new device never creates a second attribution for the same user.

5. Responses

StatusMeaning
200accepted (duplicate: true when the conversion_id was already processed)
400missing / invalid field or unsupported event type
401bad API key, bad signature, or stale timestamp
403event type not enabled for this website
500server error — safe to retry with the same conversion_id

Example success:

{ "success": true, "attribution": "locked", "affiliate_code": "YUI",
  "verified": true, "deleted": false, "duplicate": false, "commission_amount": 0 }

commission_amount is always 0 — commissions are never created by this endpoint.


6. Commission model (platform side, for context)

  • A user is eligible when: verified AND not deleted AND active within the activity window (default 30 days).
  • On the 15th and 30th of each month the platform calculates, per ambassador: net growth = eligible now − high-water mark (HWM).
  • Commission is paid only for positive net growth, once per user, at the per-user amount defined in Commission Rules.
  • The HWM only ever increases — churn never creates a negative balance, and regained users are never paid twice.

7. Retry guidance

Retry on network errors and 5xx with exponential backoff, reusing the same conversion_id. Duplicates are safely ignored.