Affiliate & Creator Tracking (Impact.com)
CardNexus uses Impact.com for affiliate and creator partner tracking. Partners share tracked links that attribute signups and purchases back to them, driving commission payouts.
This page covers the full system: domains, link formats, conversion events, attribution model, deep linking, and the code that wires it all together.
Impact.com Account
| Field | Value |
|---|---|
| Account ID | 6932348 |
| Program | CardNexus - Creator Program |
| Campaign ID | 48046 |
| Credit Policy | Last Click |
| Referral Window | 30 days |
Domains
Two external domains are involved in the tracking flow:
| Domain | Purpose | Infrastructure |
|---|---|---|
go.cardnexus.link | Impact.com tracking domain. Cloudflare-proxied to Impact. Handles click recording, attribution, and redirect to landing page. | DNS proxied to Impact |
af.cardnexus.link | Clean URL shortener for affiliates. Cloudflare Worker that mints an af_id correlation token, injects utm_* + af_id into the landing URL, then 302s to go.cardnexus.link with proper Impact URL structure. | Cloudflare Worker |
Conversion Events
Two events are reported to Impact via their Conversions API:
| Event | ActionTrackerId | When Reported | Amount | CustomerStatus |
|---|---|---|---|---|
| Signup | 68897 | New user creation (when im_ref cookie is present) | None | NEW |
| Purchase | 68898 | Order creation (one per order — a checkout with 3 sellers fires 3 events) | funds.charged (post-coupon, buyer’s currency) | EXISTING |
Both share CampaignId: 48046 and a 30-day referral window pre-filter.
Purchase fires at order creation, not completion. This gives affiliates immediate visibility into conversions. If the order is later cancelled, the conversion is reversed via Impact’s Actions API. Partial refunds update the conversion amount.
Conversion Lifecycle
Purchase conversions follow a lifecycle that mirrors the order:
| Order Event | Impact Action | API Endpoint |
|---|---|---|
| Order created | Create conversion (POST) | POST /Conversions |
| Order cancelled (full) | Reverse conversion (DELETE) | DELETE /Actions with DispositionCode |
| Partial refund processed | Update amount (PUT) | PUT /Actions with new Amount and Reason=ORDER_UPDATE |
Conversions are identified by ActionTrackerId + OrderId — no need to store Impact’s internal ActionId.
Cancellation Disposition Codes
cancelledBy | DispositionCode |
|---|---|
buyer | ITEM_RETURNED |
seller | ORDER_ERROR |
system | ORDER_ERROR |
Refund Logic
When order.refund_processed fires, the subscriber calculates newAmount = funds.charged - funds.refunded:
- If
newAmount <= 0→ reverse the conversion (same as cancellation) - If
newAmount > 0→ update the conversion amount via PUT - If the order is already in a cancelled status → skip (reversal already sent)
Attribution Model
Last-click-wins. Each time a user clicks a new affiliate link, their attribution.impact.clickId is overwritten. The most recent click gets credit for future conversions.
Attribution is stored in two places:
| Location | Purpose | Mutable? |
|---|---|---|
user.attribution.impact.{clickId, capturedAt, afId?} | Current attribution for the user. Used for signup conversion and snapshotted onto orders at checkout. | Yes — overwritten on each new affiliate click |
order.affiliateAttribution.{clickId, capturedAt, afId?} | Snapshot from buyer at order creation. Used for purchase conversion. | No — set once, never changed |
af_id — CardNexus-Side Correlation Token
af_id is a UUID minted by the af.cardnexus.link Worker on every inbound click. It is a property of an impact attribution record, not a separate attribution source. It lives alongside clickId inside attribution.impact.
Invariant: if afId is present, clickId is always present. The inverse is not true — direct Impact partner links (go.cardnexus.link/c/... shared outside our Worker) carry only clickId. A bare af_id with no im_ref is treated as a broken upstream flow and silently dropped.
Why it exists. clickId (Impact’s im_ref) is minted by Impact after our Worker 302s into their tracking domain. We can’t join our own Cloudflare Logpush rows to app-side conversions without crossing the Impact API boundary. af_id is a key we control end-to-end — from the Worker’s log row in cardnexus-datawarehouse.affiliate_logs.af_clicks all the way to user.attribution.impact.afId and order.affiliateAttribution.afId.
Example BigQuery join:
SELECT c.af_id, c.mp_id, c.event_ts, u._id AS user_id
FROM `cardnexus-datawarehouse.affiliate_logs.af_clicks` c
JOIN `<users-mirror>` u ON u.attribution.impact.afId = c.af_idCapture flow:
- Worker mints
af_idand injects it into the landing URL alongsideutm_*(see the Clean URL Shortener section for params). - Next.js middleware reads
?af_id=…and sets a first-party cookie (af_id, 30 days, alongsideim_ref). - On authenticated requests,
orpcSSRforwards the cookie asX-Affiliate-Af-Idheader alongsideX-Affiliate-Click-Id. - Backend signup middleware writes both to
user.attribution.impactwhenisNewUser. The frontend hook does the same for returning users viauser.setAffiliateAttribution. OrderOrchestratorsnapshotsafIdontoorder.affiliateAttributionat order creation, next toclickId.
No backfill: pre-existing users and orders without afId are fine. The field is forward-only and only populated for traffic routed through af.cardnexus.link.
Link Formats
Standard Impact Tracking Link
Partners get their tracking link from Impact’s dashboard:
https://go.cardnexus.link/c/{mpId}/{adId}/48046{mpId}— partner’s media partner ID (from Impact){adId}— ad/creative ID (from Impact)48046— CardNexus campaign ID (constant)
Impact records the click and redirects to the configured landing page with im_ref appended.
Deep Links (Product-Specific)
To link to a specific product, partners use the u parameter to override the landing URL:
https://go.cardnexus.link/c/{mpId}/{adId}/48046?u=https%3A%2F%2Fcardnexus.com%2Fgo%3Ftcg%3D631412%26q%3DCharizard%2BBase%2BSetImpact redirects to:
https://cardnexus.com/go?tcg=631412&q=Charizard+Base+Set&im_ref=<click-id>Clean URL Shortener (af.cardnexus.link)
af.cardnexus.link provides clean, readable URLs for affiliates. The Cloudflare Worker:
-
Mints
af_id = crypto.randomUUID()per click and logs the request to BigQuery via Logpush (cardnexus-datawarehouse.affiliate_logs.af_clicks). -
Builds the landing URL
https://cardnexus.com/go?…with injected tracking params:Param Value utm_sourceafutm_mediumaffiliateutm_campaign{mpId}utm_content{afId}utm_term{shape}(marketplace/search/general)af_id{afId} -
302s to
go.cardnexus.linkwith that URL encoded as Impact’su=param. -
Impact 302s back to
https://cardnexus.com/go?…&utm_*=…&af_id=…&im_ref=<click-id>— so the landing URL carries both ouraf_idand Impact’sim_ref.
See specs/impact-affiliate/brief-cloudflare.md for the Worker implementation brief and specs/impact-affiliate/brief-af-id.md for the app-side af_id persistence design.
Product deep-link (with marketplace ID):
af.cardnexus.link/{mpId}/tcg/{productId}
af.cardnexus.link/{mpId}/tcg/{productId}/{product name}
af.cardnexus.link/{mpId}/cm/{productId}
af.cardnexus.link/{mpId}/cm/{productId}/{product name}Search link:
af.cardnexus.link/{mpId}/{search terms}General link (explore page):
af.cardnexus.link/{mpId}Examples:
af.cardnexus.link/1111/tcg/12345af.cardnexus.link/1111/tcg/12345/Charizard%20Base%20Setaf.cardnexus.link/1111/cm/67890/Black%20Lotusaf.cardnexus.link/1111/Charizard%20Base%20Set
| Segment | Description |
|---|---|
{mpId} | Partner’s media partner ID (from Impact) |
tcg / cm | Marketplace: tcg = TCGPlayer, cm = Cardmarket |
{productId} | Product’s external marketplace ID |
{product name} | Optional — product name used as search fallback |
Product names and search terms must be URI-encoded. Spaces should be encoded as %20 (e.g. Charizard%20Base%20Set). Special characters like &, ?, #, / must also be percent-encoded.
/go Route — Query Parameters
The /go route on the app receives the redirect from Impact (after click tracking) and resolves external product IDs to internal product pages.
| Param | Description | Example |
|---|---|---|
tcg | TCGPlayer external product ID | 631412 |
cm | Cardmarket external product ID | 67890 |
q | Product name (fallback search query) | Charizard+Base+Set |
game | Game slug (optional, disambiguates search) | pokemon |
im_ref | Impact click ID (captured by middleware as cookie) | abc123 |
af_id | CardNexus Worker correlation token (captured by middleware as cookie) | a7f3-… |
utm_source, utm_medium, utm_campaign, utm_content, utm_term | Standard UTM params injected by the Worker | utm_source=af |
Resolution priority: tcg ID lookup → cm ID lookup → search by q → /explore homepage.
The route is excluded from i18n locale prefixing. It reads the NEXT_LOCALE cookie to determine the correct locale prefix for the redirect target.
UTM + tracking param forwarding. /go copies utm_*, af_id, and im_ref onto the destination URL on every redirect branch. Without this, Amplitude’s auto-capture (which reads window.location.search at SDK init on the landing page) would see an empty search and attribute all affiliate traffic as “direct”. The cookie capture in middleware is independent of this forwarding — both mechanisms run.
End-to-End Flows
Signup Conversion
Purchase Conversion (with Cancellation & Refund)
Deep Link Resolution
Last-Click-Wins (Returning Users)
Attribution Windows
| Window | Duration | Where | Purpose |
|---|---|---|---|
Cookie (im_ref) | 30 days | Browser (first-party cookie) | Captures click ID from landing URL, survives until signup or next visit |
Cookie (af_id) | 30 days | Browser (first-party cookie) | Captures CardNexus Worker correlation token, mirrors im_ref lifecycle |
Referral window (referralWindowDays) | 30 days | Backend (ConversionTrackingService) | Pre-filter — skip obviously-expired conversions before calling Impact |
| Impact’s commission window | Configurable | Impact.com dashboard | Authoritative source of truth for whether a conversion earns commission |
Our referralWindowDays is a conservative pre-filter. Impact’s own window is what ultimately determines commission eligibility.
Impact Dashboard Configuration
Payout Setup (Purchase Event)
The Purchase event (68898) payout is configured in Impact’s dashboard as a percentage of the order sale amount. The Amount we send is funds.charged — what the buyer actually paid (post-coupon, in their checkout currency). Impact normalizes currencies on their side.
To give partners a share of your commission:
- If your platform commission is ~5-8%, and you want to share 50%, set the payout percentage to ~2.5-4%
- Use Payout Groups for per-partner rates
Action Locking & Payout Scheduling
These are configured per-event-type in the Impact dashboard and control when conversions lock and when payouts are processed.
Important: Since Purchase now fires at order creation (not completion), ensure the Impact action locking window is long enough to account for possible cancellations and refund adjustments.
SQS Message Format
Messages are published to the affiliate-conversions queue as a discriminated union on campaignEventId:
// Signup
{ campaignEventId: "signup", userId, clickId, capturedAt }
// Purchase
{ campaignEventId: "purchase", userId, clickId, capturedAt, orderId, totalAmountCents, currency }
// Reversal (cancellation)
{ campaignEventId: "reversal", orderId, cancelledBy }
// Amount Update (partial refund)
{ campaignEventId: "amount_update", orderId, newAmountCents, currency }Queue config: standard queue, 1 concurrent consumer, 3 retries before DLQ, 4-day retention.
Environment Variables
| Variable | Required | Purpose |
|---|---|---|
IMPACT_ACCOUNT_SID | Yes | Impact.com advertiser account ID (for API auth) |
IMPACT_AUTH_TOKEN | Yes | Impact.com API token (Basic auth with account SID) |
If missing, conversions are silently skipped with a warning log.
How to Add a New Conversion Event
Add the event to the enum
In packages/backend/features/affiliate-tracking/src/campaigns/campaign-event.ts:
export enum CampaignEvent {
Signup = "signup",
Purchase = "purchase",
MyNewEvent = "my_new_event", // new
}Add config with campaignId and actionTrackerId
In packages/backend/features/affiliate-tracking/src/campaigns/config.ts. Get the actionTrackerId from the Impact.com dashboard (Event Types settings):
[CampaignEvent.MyNewEvent]: {
campaignId: 48046,
actionTrackerId: 99999, // from Impact dashboard
referralWindowDays: 30,
},Add payload type to the payload map
In packages/backend/features/affiliate-tracking/src/campaigns/payload-map.ts:
[CampaignEvent.MyNewEvent]: { orderId: string; totalAmountCents: number; currency: string }Add the SQS message schema
In packages/backend/queue/src/dtos/affiliate-conversion.dto.ts, add a Zod schema and include it in the discriminated union:
export const myNewEventSchema = z.object({
campaignEventId: z.literal("my_new_event"),
...baseFields,
orderId: z.string(),
totalAmountCents: z.number().int().positive(),
currency: z.string(),
})
// Add to the union:
export const affiliateConversionMessageSchema = z.discriminatedUnion(
"campaignEventId",
[signupConversionSchema, purchaseConversionSchema, myNewEventSchema],
)Add a consumer branch
In packages/backend/features/affiliate-tracking/src/conversion-tracking.consumer.ts:
case "my_new_event":
await this.conversionTrackingService.report(
CampaignEvent.MyNewEvent, userInfo,
{ orderId: message.orderId, totalAmountCents: message.totalAmountCents, currency: message.currency },
)
breakWire the publisher
Publish to the "affiliate-conversions" queue from wherever the conversion happens. For events that should only fire on completion (like purchases), use an EventBus subscriber pattern — see PurchaseConversionSubscriber for the example.