Sub-affiliate recruiting API
Endpoints, override conversion shape, webhook payloads, and apply-form semantics for the bounded 1-tier sub-affiliate model.
This is the developer-side surface for sub-affiliate recruiting: the brand-side concept page covers the "what" and "why"; this page covers the wire shapes.
There are five endpoints, one new conversion-row shape, one webhook variation, and one apply-form field. Everything is bounded to a single tier of recruitment depth; there is no chain propagation.
Endpoints
All five are mounted under /api/v1/* with the standard Authorization: Bearer rk_live_* API key auth, plus the same JWT path for the in-product dashboard.
GET /api/v1/programs/{id}/sub-affiliate-config
Scope: read. Returns the current sub-affiliate config for a campaign.
curl -sS https://api.rekomi.com/api/v1/programs/0789abcd-.../sub-affiliate-config \
-H "Authorization: Bearer $REKOMI_API_KEY"Response:
{
"enabled": true,
"percent": 10.0
}enabled: whether the recruiting feature is on for this campaign.percent: override percent on parent commission. Range 0 to 100. Whenenabledis false, this field is still returned (it just doesn't fire on new conversions).
PUT /api/v1/programs/{id}/sub-affiliate-config
Scope: read_write. Role: Owner (on JWT). Plan: Pro+. Email verification required.
Audit-logged as program.sub_affiliate_config_changed.
curl -sS -X PUT https://api.rekomi.com/api/v1/programs/0789abcd-.../sub-affiliate-config \
-H "Authorization: Bearer $REKOMI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"enabled": true,
"percent": 10
}'Response (200):
{
"enabled": true,
"percent": 10.0
}Failures:
| HTTP | error | Cause |
|---|---|---|
| 400 | validation_failed | Percent outside [0, 100] or not a number |
| 402 | plan_tier_required | Org below Pro and not trialing |
| 403 | email_not_verified | Owner exists but has not verified email |
| 403 | (empty) | Caller is not Owner |
| 404 | (empty) | Program not in your org |
GET /api/v1/me/sub-affiliate/link?programId={id}
Scope: read. Affiliate-side. Returns the calling affiliate's personal recruitment link for a campaign. Auth is the affiliate's /a JWT, not a brand API key.
curl -sS "https://api.rekomi.com/api/v1/me/sub-affiliate/link?programId=0789abcd-..." \
-H "Authorization: Bearer $AFFILIATE_JWT"Response:
{
"affiliateId": "01234567-...",
"programId": "0789abcd-...",
"programSlug": "default",
"url": "https://your-brand.example.com/p/default?recruited_by=01234567-..."
}Returns 400 sub_affiliate_recruiting_disabled if the campaign has enabled = false, and 404 if the campaign isn't visible to the calling affiliate.
GET /api/v1/me/sub-affiliate/recruits
Scope: read. Affiliate-side. Lists the calling affiliate's downline. Cursor-paginated.
curl -sS "https://api.rekomi.com/api/v1/me/sub-affiliate/recruits?take=50" \
-H "Authorization: Bearer $AFFILIATE_JWT"Response:
{
"items": [
{
"affiliateId": "fed00b00-...",
"email": "recruit@example.com",
"fullName": "Recruited Person",
"status": "Approved",
"totalCommissionGeneratedCents": 12400,
"overrideEarnedCents": 1240,
"currency": "USD",
"joinedAt": "2026-05-12T10:00:00Z"
}
],
"nextCursor": null
}totalCommissionGeneratedCents: this recruit's own commission income (excludes override rows so it never double-counts when the recruit later recruits someone else).overrideEarnedCents: total override the calling affiliate has earned from this recruit, incurrency.- Only Approved recruits accrue override income. Pending/Rejected recruits show in the list with
overrideEarnedCents = 0.
GET /api/v1/affiliates/{id}/sub-affiliates
Scope: read. Brand-side drilldown: lets a brand operator inspect any affiliate's downline. Returns a flat array (no pagination wrapper) with slightly different field names than the affiliate-side endpoint.
curl -sS "https://api.rekomi.com/api/v1/affiliates/01234567-.../sub-affiliates" \
-H "Authorization: Bearer $REKOMI_API_KEY"[
{
"id": "fed00b00-...",
"email": "recruit@example.com",
"fullName": "Recruited Person",
"status": "Approved",
"totalCommissionCents": 12400,
"overrideEarnedCents": 1240,
"currency": "USD"
}
]Override conversion row
When affiliate A recruits affiliate B, and B closes a sale, two Conversion rows are written in the same transaction:
- Parent commission row for B. Looks like a normal conversion.
AmountCentsis the sale,CommissionCentsis B's commission,AttributionMethodis whatever path attributed the sale (click,coupon,pixel,s2s, etc.).ParentAffiliateConversionIdisnull. - Override row for A.
AmountCents = 0,CommissionCents = (B.commission × Program.SubAffiliateCommissionPercent / 100), rounded down to the nearest minor unit.AttributionMethod = "sub_affiliate_override".ParentAffiliateConversionIdpoints at the parent row's id.
Example conversion list query showing both rows:
curl -sS "https://api.rekomi.com/api/v1/conversions?programId=0789abcd-...&take=50" \
-H "Authorization: Bearer $REKOMI_API_KEY"Response (relevant fields only):
{
"items": [
{
"id": "c0nv0parent-...",
"affiliateId": "01234567-B-...",
"programId": "0789abcd-...",
"amountCents": 10000,
"currency": "USD",
"commissionCents": 2000,
"attributionMethod": "click",
"parentAffiliateConversionId": null,
"status": "Approved"
},
{
"id": "c0nv0override-...",
"affiliateId": "01234567-A-...",
"programId": "0789abcd-...",
"amountCents": 0,
"currency": "USD",
"commissionCents": 200,
"attributionMethod": "sub_affiliate_override",
"parentAffiliateConversionId": "c0nv0parent-...",
"status": "Approved"
}
],
"nextCursor": null,
"hasMore": false
}Notes:
- Override rows always have
amountCents = 0. The sale amount lives on the parent row; duplicating it on the override would double-count revenue on dashboards. - Override rows always have
commissionCents > 0. If the computed override rounds to zero (very small parent commission × very small percent), the row is not created. parentAffiliateConversionIdis a foreign key to the sameConversiontable. Use it to join override rows back to the sale that generated them.
Refund cascade
When the parent conversion is refunded (partially or fully) via the Stripe webhook path, the pixel-refund endpoint, or the S2S refund endpoint, Rekomi computes the proportional fraction of the parent commission that was clawed back and applies the same fraction to the override.
- Full refund of the parent: override is fully reversed.
- Partial refund (e.g., 40% of the sale amount): both the parent commission and the override are reduced by 40%.
Both rows fire conversion.refunded webhook events. Consumers tracking lifetime earnings should subtract the commissionReversedCents from their running totals on both rows.
Webhook payloads
conversion.created (override row)
Same envelope as the normal conversion.created event. The body distinguishes override rows via attributionMethod:
{
"id": "evt_01HVABCDEFGHIJKLMN",
"type": "conversion.created",
"createdAt": "2026-05-12T03:14:25.000Z",
"organizationId": "01HORG123...",
"data": {
"conversionId": "c0nv0override-...",
"affiliateId": "01234567-A-...",
"programId": "0789abcd-...",
"amountCents": 0,
"currency": "USD",
"commissionCents": 200,
"status": "Pending",
"attributionMethod": "sub_affiliate_override",
"parentAffiliateConversionId": "c0nv0parent-...",
"type": "OneTime"
}
}conversion.refunded (override row)
Emitted on every cascade iteration where the override's RefundedAmountCents was incremented. refundedAmountCents and commissionReversedCents are the THIS-event clawback; cumulativeRefundedAmountCents is the lifetime total on the row; isFullRefund is true only on the iteration that crossed the override into fully-refunded state. Carries the parent linkage + attribution-method so consumers can route override-row events separately.
{
"id": "evt_01HVZYXWVU...",
"type": "conversion.refunded",
"createdAt": "2026-05-15T11:00:00Z",
"organizationId": "01HORG123...",
"data": {
"conversionId": "c0nv0override-...",
"affiliateId": "01234567-A-...",
"programId": "0789abcd-...",
"refundedAmountCents": 200,
"cumulativeRefundedAmountCents": 200,
"commissionReversedCents": 200,
"currency": "USD",
"isFullRefund": true,
"refundedAt": "2026-05-15T11:00:00Z",
"parentAffiliateConversionId": "c0nv0parent-...",
"attributionMethod": "sub_affiliate_override"
}
}Warning for webhook consumers
If your handler filters conversion.created events by amountCents > 0 to drop "zero-revenue" events, you will silently lose all override events. Override rows are real commission obligations with commissionCents > 0 but always amountCents = 0.
Better filters:
- Filter on
status(e.g., only act onApprovedorPaid). - Filter on
commissionCents > 0if you want anything that owes the affiliate money. - Match on
attributionMethodif you want to route override rows to a different downstream pipeline (e.g., a separate accounting bucket for recruiter income vs. direct-sale commission).
Apply form: recruitedByAffiliateId
The public apply endpoint optionally accepts a recruitedByAffiliateId field. When set, the new affiliate's RecruitedByAffiliateId is populated with that value, establishing the 1-tier link.
curl -sS https://api.rekomi.com/api/p/default/apply \
-H "Content-Type: application/json" \
-d '{
"email": "newaffiliate@example.com",
"fullName": "New Affiliate",
"recruitedByAffiliateId": "01234567-A-..."
}'Rules:
- The id must reference an
Approvedaffiliate in the same org as the campaign being applied to. Mismatches are silently dropped (the application succeeds, butRecruitedByAffiliateIdstays null). - Self-recruit attempts (i.e., the applicant's eventual id equaling the supplied id) are blocked at row creation.
- If the campaign has
enabled = false, the field is accepted on the application but the override row will never fire on sales (it's stored for auditability but is operationally inert). - The field is not discoverable from the public apply schema for SEO reasons; only callers who have a recruiter id (typically from a tagged URL) should pass it.
In practice you don't construct this by hand. The recruitment URL (returned by GET /api/v1/me/sub-affiliate/link) carries recruited_by={affiliateId} as a query param. The public apply landing page reads that param and forwards it as recruitedByAffiliateId in the form submission.
MCP tools
For AI-agent integrations, three tools wrap the brand-side endpoints:
| Tool | Maps to |
|---|---|
get_program_sub_affiliate_config | GET /api/v1/programs/{id}/sub-affiliate-config |
set_program_sub_affiliate_config | PUT /api/v1/programs/{id}/sub-affiliate-config |
list_affiliate_sub_affiliates | GET /api/v1/affiliates/{id}/sub-affiliates |
All three accept either rk_live_* (direct API key) or rk_mcp_* (relayed via mcp.rekomi.com). The affiliate-side /me/sub-affiliate/* endpoints are Clerk-JWT-only and intentionally not exposed via MCP (the relay carries brand-org identity, not per-affiliate identity). See MCP for the auth model.
See also
- Sub-affiliate recruiting (brands): concept, math, refund cascade, FAQ.
- Conversion currency: override rows carry the same
currencyfield; the override commission is denominated in the parent sale's currency. - Webhooks: event envelope, signature verification, retry semantics.
- API reference: terse endpoint signatures alongside the rest of
/api/v1/*.