Skip to Content
BackendAffiliate & Creator Tracking (Impact.com)

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

FieldValue
Account ID6932348
ProgramCardNexus - Creator Program
Campaign ID48046
Credit PolicyLast Click
Referral Window30 days

Domains

Two external domains are involved in the tracking flow:

DomainPurposeInfrastructure
go.cardnexus.linkImpact.com tracking domain. Cloudflare-proxied to Impact. Handles click recording, attribution, and redirect to landing page.DNS proxied to Impact
af.cardnexus.linkClean 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:

EventActionTrackerIdWhen ReportedAmountCustomerStatus
Signup68897New user creation (when im_ref cookie is present)NoneNEW
Purchase68898Order 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 EventImpact ActionAPI Endpoint
Order createdCreate conversion (POST)POST /Conversions
Order cancelled (full)Reverse conversion (DELETE)DELETE /Actions with DispositionCode
Partial refund processedUpdate 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

cancelledByDispositionCode
buyerITEM_RETURNED
sellerORDER_ERROR
systemORDER_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:

LocationPurposeMutable?
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_id

Capture flow:

  1. Worker mints af_id and injects it into the landing URL alongside utm_* (see the Clean URL Shortener section for params).
  2. Next.js middleware reads ?af_id=… and sets a first-party cookie (af_id, 30 days, alongside im_ref).
  3. On authenticated requests, orpcSSR forwards the cookie as X-Affiliate-Af-Id header alongside X-Affiliate-Click-Id.
  4. Backend signup middleware writes both to user.attribution.impact when isNewUser. The frontend hook does the same for returning users via user.setAffiliateAttribution.
  5. OrderOrchestrator snapshots afId onto order.affiliateAttribution at order creation, next to clickId.

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.

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.

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%2BSet

Impact redirects to:

https://cardnexus.com/go?tcg=631412&q=Charizard+Base+Set&im_ref=<click-id>

af.cardnexus.link provides clean, readable URLs for affiliates. The Cloudflare Worker:

  1. Mints af_id = crypto.randomUUID() per click and logs the request to BigQuery via Logpush (cardnexus-datawarehouse.affiliate_logs.af_clicks).

  2. Builds the landing URL https://cardnexus.com/go?… with injected tracking params:

    ParamValue
    utm_sourceaf
    utm_mediumaffiliate
    utm_campaign{mpId}
    utm_content{afId}
    utm_term{shape} (marketplace / search / general)
    af_id{afId}
  3. 302s to go.cardnexus.link with that URL encoded as Impact’s u= param.

  4. Impact 302s back to https://cardnexus.com/go?…&utm_*=…&af_id=…&im_ref=<click-id> — so the landing URL carries both our af_id and Impact’s im_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/12345
  • af.cardnexus.link/1111/tcg/12345/Charizard%20Base%20Set
  • af.cardnexus.link/1111/cm/67890/Black%20Lotus
  • af.cardnexus.link/1111/Charizard%20Base%20Set
SegmentDescription
{mpId}Partner’s media partner ID (from Impact)
tcg / cmMarketplace: 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.

ParamDescriptionExample
tcgTCGPlayer external product ID631412
cmCardmarket external product ID67890
qProduct name (fallback search query)Charizard+Base+Set
gameGame slug (optional, disambiguates search)pokemon
im_refImpact click ID (captured by middleware as cookie)abc123
af_idCardNexus Worker correlation token (captured by middleware as cookie)a7f3-…
utm_source, utm_medium, utm_campaign, utm_content, utm_termStandard UTM params injected by the Workerutm_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)

Last-Click-Wins (Returning Users)

Attribution Windows

WindowDurationWherePurpose
Cookie (im_ref)30 daysBrowser (first-party cookie)Captures click ID from landing URL, survives until signup or next visit
Cookie (af_id)30 daysBrowser (first-party cookie)Captures CardNexus Worker correlation token, mirrors im_ref lifecycle
Referral window (referralWindowDays)30 daysBackend (ConversionTrackingService)Pre-filter — skip obviously-expired conversions before calling Impact
Impact’s commission windowConfigurableImpact.com dashboardAuthoritative 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

VariableRequiredPurpose
IMPACT_ACCOUNT_SIDYesImpact.com advertiser account ID (for API auth)
IMPACT_AUTH_TOKENYesImpact.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 }, ) break

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

Code Locations

ComponentLocation
Campaign config (event IDs)packages/backend/features/affiliate-tracking/src/campaigns/
ConversionTrackingServicepackages/backend/features/affiliate-tracking/src/conversion-tracking.service.ts
SQS consumerpackages/backend/features/affiliate-tracking/src/conversion-tracking.consumer.ts
PurchaseConversionSubscriberpackages/backend/features/affiliate-tracking/src/purchase-conversion.subscriber.ts
CancellationConversionSubscriberpackages/backend/features/affiliate-tracking/src/cancellation-conversion.subscriber.ts
RefundConversionSubscriberpackages/backend/features/affiliate-tracking/src/refund-conversion.subscriber.ts
SQS message DTOspackages/backend/queue/src/dtos/affiliate-conversion.dto.ts
User model (attribution)packages/core/db/src/mongo/user.model.ts
Order model (affiliateAttribution)packages/core/db/src/mongo/marketplace/order.model.ts
resolveByExternalId contractpackages/core/api-dtos/src/lib/product/resolve-by-external-id.ts
resolveByExternalId handlerpackages/backend/api/src/lib/orpc/handlers/product/resolve-by-external-id.handler.ts
/go route handlerapps/frontend/app/go/route.ts
Frontend hookpackages/frontend/affiliate-tracking/src/use-affiliate-attribution.ts
Next.js middleware (cookie + /go bypass)apps/frontend/middleware.ts
SSR header forwardingpackages/shared/trpc/src/server/orpc-server.ts
Subscriber registrationapps/backend/src/bootstrap/event-subscribers.ts
Consumer registrationapps/backend/src/handlers/registry.ts
Cloudflare Worker briefspecs/impact-affiliate/brief-cloudflare.md
Last updated on