Partnely API
One REST API per brand. Everything the brand dashboard does is available here, plus a server-to-server way to record conversions. Base URL: https://partnely.app/api/v1.
Authentication
Generate a key under Connect store → For developers in the brand dashboard. Send it as a bearer token. Keys start with pn_live_; only a hash is stored, so copy it when it is shown.
The order postback, POST /conversions, works on every plan. The other endpoints come with the Growth plan and above; on other plans they answer plan_limit 403.
curl https://partnely.app/api/v1/me \ -H "Authorization: Bearer pn_live_…"
The tracking snippet uses a separate public key (pk_…) that is safe to embed in a web page. It can only report conversions, and those still require the brand’s approval.
Conventions
- JSON in and out. Field names are
snake_case. - Money is in integer cents (
subtotal_cents,commission_cents). The conversion postback accepts major units (subtotal: 129.00) for convenience. - Percent commissions are basis points:
1000= 10.00%. - Timestamps are ISO 8601 strings in UTC.
- Successful responses wrap the payload:
{ "data": …, "meta": { … } }. Lists paginate with?limit=(max 200) and?offset=;meta.totalis the full count. - Errors:
{ "error": { "code", "message", "details?" } }with a matching HTTP status. Codes:unauthorized401,plan_limit403,not_found404,validation_error400,bad_request400,reason_required400,invalid_state409,order_locked409,conflict409,nothing_to_pay409,currency_required409,payouts_tracked_only409,rate_limited429,internal_error500. - Requests are rate limited per address, per store, and failed keys per address. Over a limit the answer is
rate_limited429 with aRetry-Afterheader in seconds. See Rate limits.
Brand
The brand that owns the key. No secrets, API key hash, billing provider ids or billing email; has_webhook_secret and has_shopify_admin_token say whether a webhook secret and a Shopify Admin API token are saved. public_id is the store's Partnely ID (B- and a number).
Catalog
List products. Filters: active, q (text in the title, SKU, handle or vendor) and category (a kind and a key, e.g. collection:serums). Every product carries hidden_from_creators_at, read only: the time the store hid it from creators in the dashboard, else null. No write here changes it.
Create one. Body: title, url, optional external_id, sku, price_cents, image_url, categories, commission_type, commission_value.
Partial update; same fields as create plus is_active. categories replaces the product's category entries; null clears them. Collections and types from a store import stay.
Links to the product keep working and lose the association.
Bulk upsert keyed on (brand, external_id); Shopify rows also match on handle so a later import fills in what an earlier one lacked.
{ "source": "shopify", "shop_domain?": "…" } // see the three paths below
{ "source": "csv", "csv": "title,url,price\n…" }
{ "source": "json", "products": [{ "external_id": "…", "title": "…", "url": "…", "categories?": ["Serums"] }] }Every product carries categories, a list of { kind, key, name }, or null when it has none. A category rate matches on the kind and the key:
collection: a Shopify collection; the key is its handle.type: the Shopify product type; the key is the type in lower case.category: a WooCommerce category (the key is its slug), or a name sent incategoriesthrough the API or a CSV (the key is the name as a slug:Gift setsisgift-sets). The API takes at most 50 names per product.
An import writes only the kinds it reads and keeps the rest: products_json and the Partnely app read collections and the type, a custom-app token the type, WooCommerce and a JSON import category, and a CSV the kinds of its category columns. The sitemap path and a CSV without a category column keep every saved category. A CSV names them in a categories column (or category, collection, collections, product_type), several in one cell split by ; or |.
A Shopify import tries these paths in order and reports which one worked in data.catalog_source:
admin_api, first where the Partnely app is installed on the shop: the GraphQL Admin API with the app's own access, every active product, 40 a request, with the product type and up to 10 collections each (60 collections in all). Products that are not public read too.products_json: the publichttps://{shop}/products.jsonfeed. Prices, SKUs, images and the product type; no token. Off on stores that disabled the storefront JSON endpoints or sit behind a password. Collections come from/collections.jsonand each collection'sproducts.json, leaving outallandfrontpage, up to 60 collections, 2,500 products in each, 200 requests and 60 seconds. A collection past 2,500 products is read in part and the next one is still read. When that reading fails or stops at a limit, the import still succeeds,data.warningssays so, and products keep the collections that were not read.admin_api: the Admin REST API, when the brand saved a custom-app token (scopesread_products,write_discounts) under Settings and the app is not installed. Every active product, paginated; preferred over the sitemap. The product type is read; the saved collections stay.sitemap:sitemap_products_N.xml, which every store serves. Titles, handles and images only;price_centsandskuare null andexternal_idis the handle.data.warningssays so and suggests a CSV or the Admin API to fill prices.
Affiliates
An affiliate’s relationship with your program is a membership. Ids in this section are membership ids.
Members with profile and status. affiliate.public_id is the creator's Partnely ID (C- and a number), the same on every store; it is a name to find them by, never a login.
status (pending | approved | paused | rejected), commission_type, commission_value, coupon_code, stripe_promotion_code_id (the Stripe promotion code behind the creator's code, promo_…; unique per store, 409 conflict), note, reapply_after (with rejected: the date the creator may apply again; left out or null declines for good), decline_reason (with rejected: the reason the creator reads in the Not approved email, their notifications and the program page, at most 300 characters; moving a creator into rejected without it gives none, on a decline already standing leaving it out keeps it and null removes it, and it is cleared when they leave rejected; note is never shown to them). A field left out stays as saved; null clears it. commission_type and commission_value are set together: null for either clears the creator's own rate, one sent alone must complete the saved one, and a percent value is basis points up to 10000.
Links & clicks
Every tracked link with tracked_url and lifetime stats.
Raw clicks. IPs are stored hashed. Bots are flagged, not deleted.
Conversions
Each conversion carries subtotal_cents, commission_cents, status, attribution_method, source, the matched coupon_code, timestamps, and these fields worth knowing:
subtotal_cents: the commission and fee base, what the shopper paid for products after discounts, excluding tax and shipping. Gift cards are left out unless the store pays commission on gift cards (a Settings choice).currencyis the store's currency, into which an order in another currency is converted (see Currency); an order still waiting for an exchange rate, or on a store whose currency is not confirmed, keeps its own.platform_fee_cents: Partnely's fee on that order's subtotal at the brand's plan rate when the order was recorded. Zero during the free trial; reversed orders carry no fee.risk_flags: an array, usually empty, of signals worth a human look before approval.self_referral(the customer email hashes to the creator's own login),repeat_customer(the same customer bought through the same creator within 30 days),fast_order(the order landed within a minute of the click),code_spike(the daily check saw the creator's code on far more orders than usual without a click; added to pending orders after they are recorded). Flags never block recording; the order lands as pending and the brand decides.verified:truefor orders from a signed platform webhook, the order API or the brand's own entry;falsefor orders the snippet reported, since anyone holding the public key can send one. Bulk approval and auto-approval skip unverified orders; approve each by hand after checking it against the store's orders.reversed_byandreversal_reason: who reversed the order,brand(by hand or through the API),platform(a store refund or cancellation) orsystem, and the reason the creator sees.held_atandheld_reason: set while the brand holds a pending order on Orders (a return, an exchange or a dispute open on it), and the reason the creator sees. Auto-approval and bulk approval leave a held order pending; approving or reversing it clears both. Null otherwise.metadata: from the order API, up to 20 scalar values (strings, numbers, booleans) whose keys do not look personal; anything else is dropped; asubscription_idsent on a first order is kept asmetadata.subscription_id. The snippet's postback keeps no metadata and ignoressubscription_id. The commission breakdown and the rates the order was recorded with are added undermetadata.commission; a converted order carriesmetadata.original, and an order waiting for an exchange ratemetadata.fx_pending: true(see Currency).
Every order starts pending. Approval makes its commission final: an approved or paid order is never reversed, by hand or by a later refund.
date_basis says which date from and to read and the list is sorted by, newest first: placed (the default, when the shopper ordered), recorded (when the order reached Partnely) or approved (when it became final; an order never approved is left out of a range and sorted last). Anything else answers 400 bad_request.
Record an order. Idempotent on order_id: a repeat returns 200 with outcome: "duplicate". A new order returns 201. An order with no matching affiliate returns 200 with outcome: "unattributed" and a reason; it is also written to the event log.
POST /api/v1/conversions
{
"order_id": "1042",
"order_number": "#1042",
"subtotal": 129.00, // products after discounts, without tax and shipping
"currency": "USD", // optional: the store's currency when left out; another currency is converted
"occurred_at": "2026-09-10T14:02:00Z",
"click_id": "…", // from pn_click on the landing URL
"ref": "…", // or the link code from pn_ref
"coupon_codes": ["MAYA10"], // a creator's code wins over click_id and ref
"sub_id": "reel-may", // optional: the creator's own label for the placement
"customer_email": "…", // stored hashed
"subscription_id": "sub_…", // optional, on the order that starts a subscription
"line_items": [
{ "product_id": "8000000001", "sku": "MS-VITC", "title": "Vitamin C Serum", "quantity": 1, "price": 129.00, "line_total": 116.10 },
{ "title": "Gift card", "quantity": 1, "price": 50.00, "gift_card": true }
]
}The order API keeps the occurred_at it is sent (the current time when it is left out) and its metadata as described above; customer_email is stored only as a hash. A currency left out, null or blank is the store's currency.
line_items[].line_total(optional): what the shopper paid for the line, after its discounts and without tax. Sent, it replacesprice×quantityas the line's base.line_items[].gift_card(optional):truefor a gift card line. Gift card lines are left out of commission and fee unless the store pays commission on gift cards; an order of gift cards only then records nothing.subscription_id(optional): the subscription the order starts. Send it on the first order so later renewals can find the creator who was credited with it.- Renewals:
renewal: true(also1,yes,on),renewal_of(the subscription id) orrenewal_of_order(the subscription's first order id) marks a renewal order. A renewal records nothing unless the store turned on commission for renewals in Settings; then it goes to the creator credited with the subscription's first order, unless a creator code or click on the renewal itself says otherwise.
Pending → approved. The commission is final from then on. An order still waiting for an exchange rate into the store's currency answers 409 exchange_rate_pending (see Currency).
Body { "reason": "Refunded" }. reason is required (400 reason_required), at most 300 characters, and shown to the creator. Only a pending conversion can be reversed: an approved or paid one answers 409 order_locked, because approved commission is final, and one already reversed answers 409 invalid_state. The conversion records reversed_by: "brand".
Payouts
History, plus meta.owed: approved, unpaid commission per affiliate and currency, each row with its currency and bonus_cents, the bonuses owed beside the commission, which the next PayPal payout pays with it. Rows in the store's currency come first, largest first; then the other currencies alphabetically, largest first within each. Amounts in different currencies are never added up.
Answers 409 payouts_tracked_only. A creator is paid by the platform from the store's own connected PayPal, and the status of every payment comes back from PayPal, so no payout is recorded or marked paid from outside.
The payout and the conversions it covers.
Answers 409 payouts_tracked_only, for the same reason.
Campaigns
One offer sent to many creators: gifting, commission_boost, flat_fee or content. Creators apply in the app; accepting one creates a campaign link, and orders through it roll up to the campaign. A commission_type / commission_value on the campaign overrides the affiliate and brand rates for those links (product and category rates still win). Once the campaign is closed, orders placed after it closed earn the creator's normal rate (product rate, then category rate, then the affiliate's own rate, then the brand default); orders placed before keep the campaign rate. A campaign with an ends_at closes itself at the end of that day, and its orders afterwards earn the normal rate even before the close is written. A campaign opened with an opens_at still ahead is scheduled and opens by itself on that day. visible_to names the store's groups that see a campaign, or null for everyone in the program. A sale to share (type: "sale") carries its page and code in sale; it is made in the app.
List campaigns, each with stats (human clicks, orders, revenue_cents, commission_cents through campaign links) and applications (total, pending, accepted).
Create one. Saved as a draft unless status is open; product_ids must be in your catalog. opens_at and ends_at take the same formats as deadline; a day alone opens at the start of that day and ends at its end.
deliverable_kinds says how each deliverable is handed in, keyed by the line's exact wording: post (a post or video), story, link_in_bio, video_link (a link in a video description) or other. A line left out is read from its wording, and kinds for lines not in deliverables are dropped. Responses return deliverable_kinds for every line, or null when the campaign lists no deliverables.
{
"title": "Autumn routine gifting",
"type": "gifting", // gifting | commission_boost | flat_fee | content
"summary": "Two products, one Reel within 14 days of delivery.",
"brief": "…",
"product_ids": ["prod_…"],
"commission_type": "percent", "commission_value": 1500, // boost: basis points or cents
"flat_fee_cents": 15000, // flat_fee: per accepted creator
"slots": 12, "deadline": "2026-10-01",
"requirements": ["10k+ followers"], "deliverables": ["1 Reel", "Tracked link in bio for 30 days"],
"deliverable_kinds": { "1 Reel": "post", "Tracked link in bio for 30 days": "link_in_bio" }, // optional: read from the wording when left out
"is_public": true, "status": "open"
}Stats & events
Totals, commission by status, a daily series, and top affiliates.
The inbound tracking log: every webhook, pixel call, and API postback with its outcome (processed | duplicate | unattributed | rejected | error) and message. Start here when an order is missing.
Loyalty points
For a headless store, or any storefront that shows a customer their own points. Every endpoint answers 404 while the store runs no points program. A member is found by customer_id (their id here, or your own customer id as your orders report it) or by email, which is hashed on arrival and kept nowhere; no answer ever carries an address or a name.
A program hands out one of two things, and every answer says which as unit: points, where a balance is a whole number of points, or credit, where the same number is cents of the store's own currency. Nothing else about these endpoints changes between the two.
Balance, tier, the dates, and points_expire_at: when the whole balance goes if nothing else moves it, or null while the store keeps points for good.
Their ledger, newest first: delta, reason (order | signup | referral | bonus | birthday | milestone | follow | newsletter | redeem | adjust | expire | import | reversal), the store's own note and the order it came from. meta.balance is what they hold now.
{ "customer_id" | "email", "delta": 250, "note": "Made good on a late delivery" }. A change by hand, written as an adjust row with the note the customer reads on their history; the API key's prefix is kept as the actor. One change is at most 1,000,000 units either way.
What points buy today. available is false for a reward this store cannot make a code for yet, so nothing is offered that a redemption would refuse. kind is discount_amount | discount_percent | free_shipping | store_credit | free_product, and a free product names its catalog row as product_id. A store-credit program has no reward list: the balance is the reward, and the customer turns any whole unit of it into a code.
{ "customer_id" | "email", "reward_id" }, or { "customer_id" | "email", "amount_cents": 1000 } in a store-credit program. Takes the balance and makes the code, which comes back as code. This is the one place the API answers with one, since a headless store has to show it. Store credit answers code: null and lands on the customer's account at the store. Too little balance is 409 not_enough_points; an amount that is not a whole unit of the currency, or more than the balance, is 409 validation_error; a store whose credit is already on the customer's Shopify account is 409 credit_at_checkout, since there is nothing to make; a free product whose product is off sale or gone from the catalog is 409 product_off_sale with the balance untouched; and a platform that refuses the code gives the balance straight back and answers 503 unavailable.
Orders earn points on their own, from the same webhook or postback that records them; there is no endpoint for earning, and none for spending a code. Points a store gives by hand, a campaign's bonus, a birthday and a milestone are all rows in the ledger above.
Outbound webhooks
Destinations Partnely posts each event to as it happens. They come with every paid plan; the webhooks page has the headers, the signature check and every payload.
List them. A signing secret is never returned: has_secret says whether one is set. meta.events lists every event type that can be subscribed to.
Add one. Body: url (https, a public domain), events (at least one type), optional kind (webhook, the default, or slack for an incoming webhook on hooks.slack.com). The answer carries secret once for a webhook destination; it is stored encrypted and never returned again.
Remove it and its delivery log. Nothing more is sent to it.
Tracking endpoints
Sub-IDs
A creator can put their own label on any tracked link: https://partnely.app/r/{code}?sid=reel-may. The label is kept on the click (printable characters, at most 64) and copied onto the order that click earns, so the creator can tell their own placements apart on their Links page and the store can read them in Reports and in the orders CSV. The snippet keeps it beside the click and sends it as sub_id; the order API takes sub_id for a store that carries the label through its own checkout. The click's own label wins over one sent with the order, and an order a coupon code decided carries none. A sub-ID never decides who is credited and is never shown to a shopper.
Redirect
GET https://partnely.app/r/{code} logs a click and 302-redirects to the link’s destination with pn_ref={code} and pn_click={click id} appended. On a Stripe store, and for a Payment Link destination, the click id is also set as client_reference_id when the destination has none. Incoming query parameters ride along: utm_* replace the destination's own, others are added only when the destination has none, and pn_* are never taken from the visitor. A switched-off link, or one whose creator is not approved in the program, redirects without logging a click or adding pn params. HEAD redirects without logging a click.
Snippet
<script src="https://partnely.app/pn.js?k=pk_…" async></script> keeps the latest pn_click and pn_ref from the landing URL for the brand’s attribution window, writes them to the Shopify cart as note attributes, and exposes window.Partnely.track(order), which posts the order with the stored reference to POST /api/track/conversion with the public key. It sends when a click or link code is stored or the order carries coupon_codes, and retries once after Retry-After or a network error. The confirmation page usually runs before the async snippet has loaded, so queue the order instead of calling track: window.Partnely = window.Partnely || {}; (window.Partnely.q = window.Partnely.q || []).push(order). The snippet sends queued orders when it loads, and the queue keeps working afterwards.
An optional inline line before the tag, <script>window.Partnely = Object.assign(window.Partnely || {}, { landing: location.href });</script>, hands the landing address to the snippet, which Safari's private browsing otherwise hides from a script served by another site. With consent, the click is also kept in first-party cookies pn_click, pn_ref and pn_store on the store's own site (its website or Shopify domain and their subdomains) for the window, so another subdomain and server-side plugins can read it. On stores other than Shopify, the stored click is added as client_reference_id to Stripe Payment Link links, Buy Buttons and Pricing Tables that have none.
Consent: without an attribute the snippet stores the reference on the shopper's device at once, except on a Shopify store, where it follows Shopify's Customer Privacy API and waits until the store's banner reports that marketing is allowed. With data-consent="wait" on the script tag that loads pn.js (not on the inline line before it) it stores nothing until the page calls Partnely.grantConsent(); the click from the landing URL is held in memory until then. Partnely.revokeConsent() removes what was stored: storage, the cookies and the Shopify cart attributes; on Shopify, a banner answer that does not allow marketing does the same. If the cookie banner answers before the async snippet has loaded, set window.Partnely = window.Partnely || {}; window.Partnely.consent = true; instead of calling grantConsent(); the snippet reads it when it loads, and a queued order is kept.
Orders reported by the snippet are unverified (verified: false), carry the server's time instead of any occurred_at sent, and keep no metadata. Report each order one way, or send exactly the same order id from each (#1001 and 1001 count as the same): otherwise one sale is recorded twice. When a signed webhook or the order API later sends the same order id for a pending snippet order, the store's amount replaces the snippet's, and so does its attribution when it names a creator.
Webhooks by platform
Three platforms post orders straight to Partnely. Each brand's Connect store page shows its URL with the brand's public key attached. Every delivery must be signed with the webhook secret saved on the Connect store page: a store without a saved secret gets 401 webhook_secret_required, a signature that does not match gets 401 unauthorized, and nothing is recorded or reversed. Without ?key=, the ?brand={id} parameter and the X-Shopify-Shop-Domain and X-WC-Webhook-Source headers only match a store when its saved secret verifies the signature.
POST https://partnely.app/api/webhooks/shopify?key=pk_…. Topicsorders/create,orders/paid,orders/updated,orders/cancelled,refunds/create. Paid and authorized orders are recorded; an order awaiting payment (bank transfer, cash on delivery) is recorded onceorders/paidarrives. A test order is recorded and reversed at once, so it shows that tracking works but never earns. Items added to a pending order in the store raise its commission once the order is paid; items removed after payment lower it when the store refunds the difference. A custom pixel oncheckout_completed(shown on the Connect store page) reports the click for Buy it now and express checkouts to/api/track/conversionwith Shopify's order id, and a signed order that arrives after the report settles it. When the signed order arrived first (within a day), a report that carries a click recorded before that order is recorded unverified with Shopify's order id, amount and currency; any other report of that order records nothing. Signature: base64 HMAC-SHA256 inX-Shopify-Hmac-Sha256.POST https://partnely.app/api/webhooks/woocommerce?key=pk_…. Topicsorder.created,order.updated,order.deleted, andsubscription.createdon stores running WooCommerce Subscriptions. Orders inprocessingorcompletedare recorded;pendingandon-holdorders (awaiting payment) are recorded when anorder.updatedmoves them toprocessingorcompleted.cancelled,refundedandfailedreverse a pending order, so does a deleted or trashed one, and refunds listed on anorder.updatedcount as refunds. Attribution reads a creator's code incoupon_lines, thenpn_click/pn_reforder meta (copied from the snippet's cookies by the order meta code on the Connect store page), then the_wc_order_attribution_session_entrylanding URL, which WooCommerce keeps only for the visit that ends in the order. A renewal finds its first order through_subscription_renewalwith thepn_subscription_idandpn_subscription_parentmeta that code writes, or thesubscription.createdwebhook. WooCommerce's unsigned URL check (webhook_id=…) is answered without recording anything. Signature: base64 HMAC-SHA256 inX-WC-Webhook-Signature.POST https://partnely.app/api/webhooks/stripe?key=pk_…. Eventscheckout.session.completed,checkout.session.async_payment_succeeded,charge.refunded,invoice.paid,invoice_payment.paid. An unpaid completed session waits for the async payment. Subscriptions: a session or invoice that paid nothing (a free trial) is not an order; the first paid invoice is the subscription's first order, carrying the trial's click; later invoices are renewals.invoice_payment.paidlinks a subscription payment to its order socharge.refundedfinds it. A test-mode event is logged and records nothing. Attribution reads a creator's code inmetadata.couponor a promotion code whose id (promo_…) is saved on the creator's membership, thenmetadata.pn_click,metadata.pn_ref, thenclient_reference_id(a click id when 20+ characters, otherwise a link code). Signature:Stripe-Signaturewith a five-minute tolerance.
Refunds and cancellations
A store refund or cancellation reverses a pending order (reversed_by: "platform"). A partial refund on a pending order lowers its commission and platform fee in proportion to the share of the subtotal refunded, measured on the same basis (tax and shipping out; a Shopify refund of a converted order in the shop's currency, taken off at the order's fixed rate); a redelivered refund is counted once, and on Shopify and Stripe a refund or cancellation that arrives just before its order is applied when the order arrives. Once an order is approved, refunds and cancellations change nothing: they are written to the event log and the commission stays as it was.
A Shopify refund that names the items returned, on a pending order recorded item by item, lowers the commission by each item's own rate rather than in proportion: the items kept are valued again at the rates they were recorded at. A refund naming items already taken back changes nothing, and a refund naming only an amount keeps the proportion.
Every other platform
BigCommerce, Squarespace, Wix, Magento, plain WordPress and custom stacks use the snippet on the thank-you page or POST /api/v1/conversions from the server. Same attribution rules, same ledger. The Connect store page shows the exact code for the store's platform. Link tracking needs the confirmation page on the store's own domain or a subdomain of it. The BigCommerce and Squarespace confirmation pages cannot read discount codes, so orders there with only a creator's code need the order API.
Rate limits
The tracking endpoints, the API and the webhooks are limited per minute, well above what real traffic needs. Over a limit the answer is 429 with Retry-After in seconds: rate_limited JSON from /api/v1, /api/track and the webhooks, a short page from /r when one address opens too many links, and a script comment from /pn.js.
/r/{code}: per address (429), and per address on one link: past that the visitor still lands on the store, without attribution and without a click./pn.js: per address only./api/track/conversion: per address. Per store, only postbacks that record nothing (invalid, unattributed or duplicate) count, and past that limit a new attributed order is still accepted./api/v1: per address, per store, and failed keys per address.- Webhooks: only refused deliveries are limited per sending address; signed deliveries are limited per store, counted after the signature verifies.
An IPv4 address counts on its own; an IPv6 address counts by its /64.
Attribution rules
For each incoming order, hints are tried in this order and the first that resolves to an approved creator or affiliate in the program wins:
- a coupon code that belongs to an approved creator's membership in the program (with several, the first on the order), or a Stripe promotion code id saved on one; it wins over any click
- clicks: every click id on the order is checked (explicit
click_idfrom the API or snippet, thepn_clickorder attribute written to the Shopify cart or WooCommerce order meta, andpn_clickin the landing URL, Shopifylanding_siteor WooCommerce_wc_order_attribution_session_entry). The approved one with the latest click inside the brand’s attribution window, measured to the order time, wins; on a tie, the source listed first. - link codes (
ref,pn_ref), only where no click decided: a link code travels with its click id, and when that click is recorded for this brand the click decides, pass or fail. So a click past the window never earns through the link code beside it. A link code counts when its source has no click id, or the click is not recorded, and only while the link is switched on. - a subscription renewal, when the store pays commission on renewals: the creator credited with the subscription's first order, with no window
A general store code that belongs to no approved creator does not count, and the clicks decide. Switching a link off stops new clicks and orders that carry only its link code; orders from clicks made while it was on still earn within the window. Pausing the creator's membership stops all earnings.
Each order is credited to one creator. Orders deduplicate on (brand, order_id). Orders are pending until approved, and approved orders are final. When nothing resolves, the event log records every reason.
Commission rules
Precedence per order line: product rate → category rate → campaign rate → affiliate override → brand default. A closed campaign's rate applies only to orders placed before it closed, or before its ends_at.
- The base is what the shopper paid for products: after discounts, excluding tax (VAT or sales tax) and shipping, on every platform. Gift cards count only when the store pays commission on gift cards; renewals of a subscription earn only when the store pays commission on renewals.
- Percent rates apply to the line total after its discounts (or the subtotal when no line items are sent). Line totals never add up to more than the subtotal.
- A fixed rate at the product or category level pays per unit on a line the shopper paid something for; at the campaign, affiliate or brand level it pays once per order.
- Orders worth nothing (a base of 0, such as an order fully discounted by a creator's code): a fixed rate at the campaign, affiliate or brand level pays on them while the store's setting “Pay a fixed per-order commission on orders worth nothing” is on, the default. Percent rates and per-unit product and category amounts never pay on them.
- A category rate applies to a line whose catalog product has no rate of its own and is in that collection, type or category. When several match, the one that pays most on that line applies; on a tie, the oldest rate.
- Line items are matched to your catalog by the platform’s
product_id, then bysku. Unmatched lines use the campaign, affiliate or brand rate. - The program's terms in force when the order was placed apply, whenever the order arrives: the default rate, the attribution window, renewals, gift cards and fixed commission on orders worth nothing. An order placed before an announced change takes effect keeps the old terms even when it is recorded after.
- Rates are fixed when the order is recorded. When a pending order is edited in the store, its lines keep the rates recorded for them (matched by product and SKU, so each variant keeps its own) and the platform fee keeps its rate; a line the edit adds, a new variant included, takes its product or category rate, else the order's base rate. Changing a rate or the plan later never changes a recorded order.
- The full breakdown is stored on each conversion under
metadata.commission: per line theratewith itslevel(product,category,campaign,affiliateorbrand),product_keyandsku, plusbase(the rate of lines without a product or category rate),fixed_on_free_ordersandfee_bps.
Currency
The store's currency is the program's currency. Once it is confirmed (chosen under Settings, or set by a Shopify store's first order), an order in another currency is converted into it when the order is recorded, at the ECB euro reference rate published on or before the order's date, crossed through the euro. That rate is fixed for the order. Commission (fixed amounts included), the platform fee, the minimum payout and payouts are then all in the store's currency.
metadata.originalkeeps the order as it came:currency,subtotal_centsandgift_card_centsin that currency,rate_micros(units of the store's currency per unit of the order's, × 1,000,000),from_eur_microsandto_eur_micros(units of each currency per euro, × 1,000,000, which the amounts are converted with),rate_dateandsource(ECB).- No rate from the seven days up to the order's date (a currency the ECB does not publish, or rates not yet fetched): the order is recorded in its own currency with
metadata.fx_pending: true. It cannot be approved until it is converted: bulk and automatic approval leave it, and approving it by hand answers 409exchange_rate_pending. The daily job converts it once a rate is stored, while it is still pending. - A store whose currency is not confirmed records each order in its own currency, and the Connect store page names orders in other currencies.