Guides
Connecting a merchant
Create the merchant's live company, configure a program, register a partner or publish its signup page, and issue a scoped server key. The dashboard's Developers page also creates publishable tracking keys, and Settings, then Connections, adds provider connections. Each credential permits different operations.
| Credential | Where it belongs | What it permits |
|---|---|---|
| Server API key | Merchant backend | Its selected company operations |
| Publishable tracking key | Website or mobile app | Referral capture and placement reads |
| Grizzlee webhook signing secret | Merchant backend | Verification of outgoing events |
| Payment provider credentials | Grizzlee Settings > Connections | That merchant's provider adapter |
Install the SDK with npm install @grizzlee/sdk. It requires Node >= 22 and supports ESM and CommonJS. See the SDK reference for usage. HTTP clients can use the API contract independently. An order-only program credits a referral to one purchase.
Sync partner enrollments
upsertPartner creates or refreshes a partner by your externalId. Use additive fields when the dashboard or placement embed sessions can enroll the same partner:
const { programId } = await grizzlee.getPlacement("customer_referral");
await grizzlee.upsertPartner({
externalId: "customer-123",
customerIds: ["customer-123"],
name: "Alex Store",
email: "alex@example.com",
addProgramIds: [programId],
});
await grizzlee.updatePartnerByExternalId("customer-123", {
removeProgramIds: [oldProgramId],
});programIds replaces the whole enrollment list. Do not use it in a sync running alongside dashboard or placement enrollment. Omit it to preserve enrollments, or use addProgramIds and removeProgramIds to change individual programs. Each field accepts up to 100 entries. Repeated additions and removals of missing enrollments do nothing. Moving a placement key to another program does not remove earlier enrollments. Grizzlee rejects a program on both sides with 422 conflicting_programs and validates every entry before writing anything. These fields also work on registerPartner and updates by internal ID. Keep the server key on your backend.
Tracking methods
Programs configure tracking.links, tracking.qrCodes, tracking.attributionCodes and tracking.providerPromoCodes independently of kind. New programs enable links, QR codes and typed attribution codes by default. Provider promo codes start off and need an active Stripe or Shopify connection. Existing affiliate programs keep typed codes off after migration. Turn on Referral codes under Advanced in the program editor to accept them without changing kind. Native in_app captures use the links switch.
Partner promo codes
An attribution code only identifies a partner. Your checkout applies the offer. A provider promo code is a separate discount created in your connected Stripe or Shopify account. In the program editor, give the friend a discount on Rewards and check that Stripe or Shopify discount codes is on under Advanced, Sharing. The dashboard turns it on for a new program with a friend discount when a connection exists. Then create a code in the partner's details.
Stripe needs a secret or restricted key with Coupons write and Promotion Codes write access. Reading applied discounts also needs Invoices and Checkout Sessions read access, alongside the existing charge and refund permissions. A webhook-only connection cannot create codes. Grant the restricted key these permissions, or create a connection with a suitable key. A denied request names the missing permission. Grizzlee never substitutes an attribution code.
Shopify needs write_discounts, which includes discount reads, alongside its existing order and customer reads and read_all_orders. Reauthorize or update the app's scopes before creating codes. The shop currency must match the program currency.
Supported offers use a fixed amount or a percentage, with an optional minimum order amount. Stripe uses a once-only coupon restricted to first-time transactions, so orders must be 1 and newCustomersOnly must be true. Stripe decides first-time eligibility from its account history. Merchants whose new-customer policy differs must use attribution codes and apply the discount themselves. Shopify uses a basic discount with one use per customer and one subscription billing cycle, so orders must be 1 and newCustomersOnly must be false. New-customer targeting would need a merchant-managed customer segment, which this adapter does not create.
Both adapters refuse percentage maxDiscountCents, category rules, multiple offer orders, returnOnFullRefund and expiresAfterDays measured from each customer's referral. Provider expiry dates are global dates and cannot express that rule. Stripe has no percentage discount ceiling. These requests return unsupported_promo_incentive with the unsupported rule. Grizzlee does not create a different discount.
const promo = await grizzlee.createPartnerPromoCode(
{ externalId: "creator-alex" },
{
programId: creatorProgram.id,
integrationId: stripeConnectionId,
code: "ALEX20",
},
);
const renamed = await grizzlee.renamePartnerPromoCode(
{ externalId: "creator-alex" },
promo.id,
{ code: "ALEX-NEW" },
);
await grizzlee.deactivatePartnerPromoCode(
{ externalId: "creator-alex" },
renamed.id,
);The SDK's promo writes need partners:write; listPartnerPromoCodes needs read. A code keeps its original friend offer after program edits. Stripe renames create a replacement with that offer and deactivate the old code. Shopify renames keep the same discount and redemption history. Both retain the old-name mapping for delayed orders. Deactivation stops future provider redemptions. Old mappings stay for delayed orders and refunds. An already discounted checkout can finish after deactivation and still credit its partner. Turning the program's switch off stops attribution, but does not deactivate the provider discount. Deactivate each code to stop its discount too.
For Stripe Checkout, set allow_promotion_codes: true so customers can enter their codes. The charge still needs a Stripe customer or grizzlee_customer_id, and the connection still needs its configured commissionable amount.
Stripe reads promotion IDs from invoice discounts or the Checkout Session for a successful charge. Shopify reads discount_codes on a paid order. No link, receipt or referral metadata is required. These become code candidates at the order time, alongside metadata candidates, under the same newest-valid and qualified-attribution rules. Grizzlee tracks only codes it created through the connection, not arbitrary provider coupons.
The provider applies the discount before Grizzlee receives the paid order. Grizzlee's qualification and fraud rules decide partner credit. They do not reverse a provider discount already applied.
Provider discounts enforce redemption limits per code. Grizzlee does not reserve these discounts through the checkout hold API. Use attribution codes and your checkout for a customer limit shared across every partner code.
Pending operations keep their IDs and original offers for retries. Discounts start inactive, and Grizzlee saves their remote IDs before activation. Cancelling an attempt without a saved remote ID retires its local name. Any provider objects created by that attempt remain inactive. Repeating the same create or rename resumes it after a timeout. Stripe uses saved IDs and idempotency keys. Shopify checks the saved discount title before resuming. Inspect pending codes in the partner details, then choose Retry creation or Deactivate code. Cancel pending rename restores the original Shopify code name without resetting its use history.
Website and mobile capture
Use the public tracking client when a customer follows a referral link. A QR code encodes that same link. Your website or app must capture it.
import { GrizzleeTrackingClient } from "@grizzlee/sdk/tracking";
const tracking = new GrizzleeTrackingClient({
baseUrl: "https://app.grizzlee.ca",
publishableKey: "grizzlee_pub_REPLACE_WITH_YOUR_PUBLISHABLE_KEY",
storage: window.localStorage,
});
await tracking.captureFromUrl(window.location.href);
const receipt = await tracking.receipt(programId);Use a stable placement key when your app needs a program ID. Point customer_referral at a program with the server SDK's setPlacement, or in the dashboard under Developer, Program keys. Then resolve it with the publishable key:
const placement = await tracking.resolvePlacement("customer_referral");
const referral = await tracking.receipt(placement.programId);resolvePlacement(key) returns { placement, programId, active }. It uses GET /api/tracking/:key/placements/:placementKey with the same origin and native-key rules as capture. It returns 404 for an unknown placement. A paused program returns active: false. Resolve when opening the screen, so moving the key to another program takes effect without a merchant deploy.
Capture and install claim redemption responses include placements: string[], the keys currently mapped to the captured program. Typed-code and referral-claim responses include the same list. A claim without an attribution returns []. Old stored receipts may have no placement list. Resolve the placement again when you need its current program.
Configure the website's exact origin on its tracking key. No trailing slash or path belongs in an origin. A new capture replaces the stored receipt. Checkout chooses the newest valid candidate while customer attribution is open. Customer referral ownership locks after a qualifying purchase. An order-only program binds ownership to the chosen order instead.
For React Native, pass an asynchronous storage implementation with getItem, setItem and removeItem, and enable native capture on the tracking key. Feed the app's incoming deep-link URL to captureFromUrl, or use capture with channel: "in_app". Native-only keys can have an empty website-origin list. The SDK does not recover a referral across a fresh app installation. That needs deferred install attribution.
Send every saved receipt and any typed code together as candidates when the customer registers or checks out. Your backend chooses the customer ID from its authenticated account, then claims it before recording payment.
import { GrizzleeServerClient } from "@grizzlee/sdk";
const grizzlee = new GrizzleeServerClient({
baseUrl: process.env.GRIZZLEE_ORIGIN!,
apiKey: process.env.GRIZZLEE_SERVER_KEY!,
});
await grizzlee.claimReferral({
candidates: [{ type: "receipt", receipt: receivedReceipt }],
customerId: authenticatedCustomer.id,
});Claiming requires partners:write. Grizzlee skips receipts from another company, expired receipts, revoked tracking keys and captures owned by another customer. The response reports the winner and each skipped list index in referralSelection. Publishable keys and allowed origins do not prove that a device is trustworthy. They never authorize a payment amount.
An only_order program also requires the merchant's order ID when claiming a receipt. One receipt cannot be reused for another order. Prefer grizzlee.completeOrder(event, { candidates }) to claim and record payment atomically. Store the receipt with the pending checkout on your backend. After your backend accepts it, call tracking.forgetAfterOrder(programId, usedReceipt) in the app.
Script tag
A site that takes payment through Stripe Payment Links, a Stripe pricing table or buy button, a Shopify store, Lemon Squeezy checkout links or Paddle.js checkout buttons can capture referrals with one tag and no SDK. Developers shows it with the company's key when you edit a tracking key:
<script
src="https://app.grizzlee.ca/js/grizzlee.js"
data-key="grizzlee_pub_REPLACE_WITH_YOUR_PUBLISHABLE_KEY"
async
></script>Add the site's exact origin to the tracking key first. The capture endpoint applies the same origin rules as the SDK, so an unlisted origin captures nothing and the browser only reports a failed request. A data-key that is not a publishable key logs one console warning and the script does nothing else.
On each page load the script reads the page URL the way captureFromUrl does: grizzlee_ref, grizzlee_program and grizzlee_channel first, then ref, program and channel. A link without a program sends the page's origin and path as destination. It captures with POST /api/tracking/:key/capture and stores the receipt in localStorage under the same key and format as GrizzleeTrackingClient with storage: window.localStorage, so adding the SDK later reads the same receipts. A new capture replaces the receipt stored for its program. Expired receipts are removed. It sets no cookies and sends nothing but the referral from the URL.
The script serves one unversioned URL with a one-hour cache, so fixes reach every site without a new snippet. Grizzlee.version reports the running version. Loading the script twice keeps the first copy.
What the script fills in
On load, and again once the capture settles, the script writes the newest receipt's checkoutReference into these. A MutationObserver keeps them current as the page changes:
- Links to
https://buy.stripe.com/,book.stripe.comanddonate.stripe.comgetclient_reference_id. A value you set yourself stays. Only an empty value or an earlier Grizzlee reference is replaced. Payment Links on a custom domain are not detected. Add the reference withGrizzlee.checkoutReference()yourself. - Links to a
lemonsqueezy.comcheckout getcheckout[custom][grizzlee], including overlay buttons. Lemon Squeezy passes it to the order asmeta.custom_data.grizzlee. Checkout links always use the store'slemonsqueezy.comaddress. - Elements with the
paddle_buttonclass getgrizzleemerged into theirdata-custom-dataJSON. Paddle.js copies it to the transaction'scustom_data. Invalid JSON is left alone. Code that callsPaddle.Checkout.openpassescustomDataitself. See Paddle. <stripe-pricing-table>and<stripe-buy-button>getclient-reference-id. An element Stripe has already drawn is replaced by a copy with the attribute, so it draws again with the reference.- Inputs with
data-grizzlee="checkout-reference"get the reference. Inputs withdata-grizzlee="candidates"get the JSON candidate list, ready forcompleteOrderorclaimReferralon your backend. - On a Shopify storefront, the cart gets a
grizzlee_candidatesattribute with the same JSON list, throughcart/update.jsunderShopify.routes.root. It writes only when the value changes. The attribute shows on the order in Shopify admin.
A merchant whose own code needs the values can use the global:
<script>
window.Grizzlee = window.Grizzlee || [];
Grizzlee.push(function (grizzlee) {
const reference = grizzlee.checkoutReference(); // or checkoutReference(programId)
const receipt = grizzlee.receipt(programId); // the stored receipt, or null
const candidates = grizzlee.candidates(); // [{ type: "receipt", receipt: "grz_..." }]
});
</script>Callbacks pushed before the script loads run once the capture settles. After that, Grizzlee.ready(callback) and Grizzlee.push(callback) run the callback right away. Without a program ID, receipt() and checkoutReference() use the receipt the script captured last, or the stored receipt that expires last.
Checkout paths
| Checkout | What records the referral |
|---|---|
| Payment Link, pricing table or buy button, subscription | The Stripe connection with Record paid invoices as orders on. The first paid invoice finds the Checkout Session by subscription and reads client_reference_id. |
| Payment Link or buy button, one-time payment | The charge's Checkout Session, found by PaymentIntent. The connection's key needs Checkout Sessions read access. Without it, the charge is recorded with no referral. |
| Stripe Checkout your server creates | Your server. Send Grizzlee.checkoutReference() with the checkout request and put it in client_reference_id. See Stripe subscriptions. |
| Shopify online store checkout | The Shopify connection reads grizzlee_candidates from the order's note attributes. |
| Lemon Squeezy checkout link or overlay | The Lemon Squeezy connection reads meta.custom_data.grizzlee on order_created. |
| Paddle.js checkout button | The Paddle connection reads custom_data.grizzlee on the first transaction. |
| Chargebee hosted page or drop-in checkout | Your server or page. The script can't fill it. See Chargebee. |
Each path also needs the provider connected in Settings, then Connections. The script only captures. It never reports or creates a payment.
Limits:
- Stripe creates no Customer for a one-time Payment Link in its default
customer_creation: if_requiredmode. A charge without a customer orgrizzlee_customer_idwaits in needs action. Setcustomer_creation: "always"on the link when you create or update it through the API. The Payment Link editor in the Stripe Dashboard has no setting for it. - Don't use a Payment Link's post-purchase invoice to get a customer while Record paid invoices as orders is on. Stripe doesn't document whether that invoice lists its PaymentIntent in invoice payments. If it doesn't, Grizzlee can record the payment twice, once as a charge order and once as an invoice order, and reward the partner twice. Grizzlee can't yet promise to record it once.
- With invoices off, Grizzlee records a subscription's charges, and a subscription's PaymentIntent does not lead to its Checkout Session. So a subscription started from a Payment Link, pricing table or buy button gets no referral. Turn invoices on.
- Shopify express checkout buttons on product pages, such as Buy it now, skip the cart and its attributes.
- Capture needs JavaScript and
localStorage. A browser that blocks storage captures the visit but keeps no receipt for checkout. - A site with a Content Security Policy must allow
https://app.grizzlee.cainscript-srcandconnect-src. Otherwise the script does nothing.
Choose the reward amount
A merchant's backend supplies the amount that its percentage program uses. A marketplace can send its earned fee without supplying the full customer order value.
await grizzlee.completeOrder(
{
type: "order.completed",
eventId: payment.id,
orderId: order.id,
customerId: customer.id,
newCustomer: order.newCustomer, // saved from your server's order history
currency: "CAD",
commissionableCents: payment.feeCents, // The merchant fee before the discount
incentiveAppliedCents: order.discountCents, // The discount on the purchase
occurredAt: payment.confirmedAt,
},
{
candidates: order.candidates,
reservationId: order.reservationId, // from reserveIncentive when the order was placed
},
);Merchants calculate the reward base on their backend and send it as commissionableCents. A marketplace can send its fee before the friend discount and configure a 5% commission at 500 basis points. A store QR program can use kind: "referral" and duration: { type: "only_order" }. Set rewardType: "cash", delivery: { cash: "merchant" } and reviewRewards: false when your backend pays approved rewards.
For a friend offer of 20% off the first purchase, use friendIncentive: { type: "percent", rateBps: 2000, orders: 1, newCustomersOnly: true }. If the purchase is $100 and the merchant fee is $20, the friend gets $20 off and a 5% partner commission on the fee earns $1. Reserve the discount when pricing, lock it to the order if payment comes later, and send the applied discount with the receipt and reservation. See friend incentives.
A store can send its purchase subtotal, a subscription can send an eligible renewal amount, and a service can send the value of eligible work. Every order requires commissionableCents, including fixed-reward orders. Include totalCents separately for purchase reporting.
Customer age schedules still change the percentage after the configured months. The schedule applies to the supplied amount. If a contract promises a percentage of GMV, send GMV as commissionableCents.
Keep the same event ID and payload when retrying. A repeated ID with different instructions returns 409. Send newCustomer from your own history on referred orders, and skip unrelated orders and past order-count imports. Without the field, report every completed order and import earlier purchases with POST /customers/import so Grizzlee can determine newness. Grizzlee still returns customerOrderNumber for the orders it records. See Purchase numbers. Send a cumulative successful refund to reverse a reward. Refunds keep the original earned rules, including phases, bonuses and caps.
Stripe
Add a Stripe connection in Settings, then Connections. Register its displayed webhook URL in that merchant's Stripe account. Subscribe to charge.succeeded, charge.refunded, refund.created and refund.updated. Refund verification needs a private key with charge and refund read permissions. With Checkout Sessions read access too, a charge also counts the checkoutReference or full receipt in client_reference_id on the Checkout Session that took the payment, as the script tag sets on Payment Links. A key without that access records the charge without it. For commission on subscription renewals, see Stripe subscriptions.
Supply metadata on the Charge that appears in those events. PaymentIntent metadata is not retroactively copied to an existing Charge. Set it before confirmation, or update the Charge explicitly before sending a correction.
| Metadata | Purpose |
|---|---|
grizzlee_new_customer | Optional true or false from your own order history |
grizzlee_customer_order_number | Optional. Kept as reported when it differs from Grizzlee |
grizzlee_customer_id | Merchant customer ID, otherwise the Stripe customer ID |
grizzlee_program_id, grizzlee_referral_code | Optional referral when no receipt was claimed earlier |
grizzlee_candidates | JSON list of saved receipt and typed code candidates |
grizzlee_total_cents | What the customer paid. Defaults to the captured payment |
grizzlee_commissionable_cents | Merchant-calculated amount for percentage rewards |
grizzlee_categories | Optional order categories, comma-separated |
grizzlee_categories lists the order's categories, such as standard,express. Up to 20, each 1 to 40 letters, digits, hyphens or underscores. Grizzlee lowercases them. Stripe caps a metadata value at 500 characters. A value Grizzlee cannot read does not hold up the purchase: Grizzlee records the order without categories and shows a note on the processed event. Programs that include only some categories then skip the order. See Order categories.
The adapter accepts grizzlee_new_customer on a Charge or Invoice as the text true or false from your own order history. When the metadata is missing, the adapter omits newCustomer and Grizzlee uses its own order count. Normalized events keep the supplied answer for retries.
Choose metadata mode to send grizzlee_commissionable_cents. Captured-charge mode uses amount_captured as commissionableCents, including tax and shipping in the charge. It does not infer a pre-tax subtotal.
Partial refunds in metadata mode need grizzlee_refunded_commissionable_cents, the cumulative successful refund of the supplied amount. Optional grizzlee_refunded_total_cents overrides the reporting refund, which otherwise uses the confirmed successful refunds. Full successful refunds reverse every associated reward. Grizzlee reads successful Stripe refunds and freezes the normalized event before processing, so retries retain the same financial instructions.
When refund.updated reports a refund as failed or canceled, Grizzlee adds up the charge's refunds that are still successful and sends that lower total, which restores the reward. In charged amount mode this is automatic. In metadata mode the refunded amounts still come from the charge metadata, so lower grizzlee_refunded_commissionable_cents and grizzlee_refunded_total_cents on the charge before Stripe sends the update. Otherwise the event changes nothing or waits in needs action.
Subscribe to transfer.reversed on this merchant account's webhook endpoint when Grizzlee sends payouts. Full and partial reversals restore returned cash to the partner's payable balance, append delivery history and send payout.reversed. Repeated or older events do not restore cash twice. Keep the connection active and select payout.reversed on your outgoing Grizzlee endpoint.
Stripe transfers require a key with Transfers create and read access, a funded merchant Stripe balance and an existing recipient acct_... connected account. Grizzlee creates transfers and reads them to verify or reconcile payouts. Reserve them from the payouts section, or register each partner's account and name the connection in the payout schedule to send scheduled payouts automatically. See Stripe transfers. These are transfers to connected accounts. Stripe handles their later bank payouts. An uncertain transfer remains reserved; use its verified tr_... reference to settle it. Do not create another payout to replace an uncertain one.
Stripe subscriptions
A Stripe connection can record each paid invoice as an order, so every subscription renewal earns like a repeat purchase. The merchant writes no code beyond putting the referral on the checkout. Turn on Record paid invoices as orders when creating the connection, or later on the connection in Settings, then Connections.
- Give the connection a secret key or a restricted key. Recording invoices needs read access to Charges, Refunds, Invoices, Subscriptions, Customers, Checkout Sessions and Credit notes. Stripe payouts also need Transfers create and read access. Grizzlee refuses to record invoices without a key.
- Subscribe the endpoint to
invoice.paid,credit_note.createdandcredit_note.voided, as well ascharge.succeeded,charge.refunded,refund.createdandrefund.updated. - Create a percentage program with
duration: { "type": "months", "months": 12 }, orfirst_n_ordersorlifetime. The duration table in the API contract explains each. - Capture the referral with the tracking client and put its
checkoutReferencein the Checkout Session'sclient_reference_id.
const referral = await tracking.receipt(programId); // in the browser
// On your server, with the reference the browser sent:
await stripe.checkout.sessions.create({
mode: "subscription",
line_items: [{ price: "price_monthly_50", quantity: 1 }],
client_reference_id: referral?.checkoutReference,
success_url: "https://your-business.com/welcome",
});What an invoice earns
Grizzlee reads the invoice from Stripe when invoice.paid arrives. The order ID is stripe: and the invoice ID, so an invoice is one order however often Stripe sends it. Charges outside invoices still count on their own. The order also gets the card fingerprint of the charge that paid the invoice as a hashed identity signal. Most charges come with the invoice's payments, so this usually costs no extra Stripe call. Otherwise Grizzlee reads one charge. It needs no permission beyond the Charges read access above, and a key that lacks it records the invoice without a fingerprint.
A Stripe payment is either its own charge order or part of invoice orders, never both. One payment can pay several invoices, and Grizzlee saves what it gave each one. It saves this in the same transaction that records the order:
-
With invoices on, a charge that Stripe links to an invoice is skipped when the invoice's order recorded it or its
invoice.paidevent is still to be processed. If that event was ignored, for example while invoices were off, or Stripe sent none within an hour of the charge, the charge becomes its own order. -
A charge from a PaymentIntent that Stripe has not linked to an invoice yet waits, retrying, for up to an hour after the charge. If no invoice claims it by then, it becomes its own order.
-
An invoice whose payment is already its own charge order is not recorded again. That happens for charges recorded before invoices were turned on, or charges that waited out the hour. The payment stays a charge order and the event shows why it was ignored.
-
Refunds follow the orders that recorded the payment, even after invoices are turned off. A refund of a payment no order has recorded yet waits, retrying, until one does. A refund of a payment split across invoices adjusts each invoice's order. The first keeps the Stripe event ID and the others add their invoice ID to it.
-
Credit notes adjust invoices recorded as orders. A credit note for an invoice whose order is still to come waits for it.
-
Amount.
amount_paid, at most the invoice total, without its tax share:amount_paid × total_excluding_tax ÷ total, rounded down. Grizzlee sends it ascommissionableCentsand sendsamount_paidastotalCents. The connection's commission amount setting andgrizzlee_commissionable_centsapply to charges only. -
Discounts. Coupons and discounts lower the commission, because the customer paid less.
-
Upgrades and proration. Each invoice earns on what it collected. An upgrade's proration invoice earns on the new price for the rest of the period minus the credit for unused time.
-
Downgrades and customer balance. A downgrade usually charges nothing and credits the customer balance. The balance lowers the next invoice's
amount_paid, so the next commission is lower. Nothing earned is reversed. -
Failed payments. An invoice earns only once Stripe marks it paid. A renewal paid after retries earns then. An invoice never paid never earns.
-
Zero-amount invoices. Trials, 100% coupons and invoices the balance covered in full are ignored. They are not purchases and do not start a
monthsduration. -
Currency. The invoice currency must be the company currency. Any other waits in needs action with the currency mismatch.
-
Order time. The invoice's
period_end. For a subscription invoice it is when the billing period it pays for begins, so a renewal paid late keeps its place in the billing cycle and a 12-month program pays 12 monthly invoices. -
Other invoices. Invoices outside subscriptions are orders too. Payments Stripe records in
amount_paid_off_stripeearn nothing.
Customers and referrals
On a Stripe customer's first paid invoice, Grizzlee chooses the customer ID and looks for a referral, then saves the link from the Stripe customer to that customer ID. Every later invoice for that Stripe customer uses the saved ID, so renewals keep the referral without any metadata. Correcting the mapping of an invoice event in the dashboard replaces the saved ID for later invoices. An invoice already recorded keeps the customer it was recorded with. Nothing else replaces the saved ID: when two first invoices of one Stripe customer are processed at once, the first one saved wins and the other is recorded under the same customer ID. Grizzlee also keeps every customer ID a Stripe customer was linked to, so erasing an earlier customer still removes that Stripe customer's unprocessed webhook bodies.
The customer ID is the first of these:
grizzlee_customer_idmetadata on the Checkout Session, subscription, subscription details on the invoice, customer or invoice, in that order.- With Find referred customers by email on, and no receipt, reference or referral code found in the sources below: the one earlier customer whose recorded email signal matches the Stripe customer's email and who has a referral. Two or more matches count as none.
- The Stripe customer ID, such as
cus_123. Charges use the same default, so a customer's one-off payments and invoices share one purchase count.
Customer ID lookup checks the Checkout Session, subscription, subscription details on the invoice at parent.subscription_details.metadata, customer and invoice, in that order. Referral selection collects candidates from every source and uses grizzlee_program_id from the same source as each legacy code.
The adapter collects referral candidates from these sources:
- A
checkoutReferenceor full receipt inclient_reference_idon the Checkout Session that created the subscription. For an invoice outside subscriptions, such as a Payment Link's post-purchase invoice, Grizzlee finds the Checkout Session through the invoice's payment. Grizzlee ignores any otherclient_reference_id, such as your own user ID. - References, full receipts and
grizzlee_candidatesJSON lists in metadata on the sources above. - Applied partner promotion codes in the invoice discounts.
grizzlee_referral_codeorgrizzlee_refmetadata with a partner's referral code. Addgrizzlee_program_idon the same source when the partner belongs to more than one active order program. A code that names no single program is skipped withwrong_program.
Grizzlee selects and claims candidates for the customer as POST /referrals/claim does, in the same transaction as the order. Qualified customer attributions keep their partner. The order's referralSelection reports the retained partner or winning candidate and each skipped reason. Expired, revoked, already owned or blocked candidates do not reject the invoice.
Email lookup is off by default. It helps when your app recorded the referral and the customer's email under your own customer ID, for example with attachReferralCode or completeOrder signals, and the subscription started in Stripe without that ID. Grizzlee compares keyed hashes of normalized emails. An email is weaker evidence than an ID: a shared or reused address moves the subscription to the other customer.
A months duration counts from the customer's first rewarded order in the program. For a new subscriber that is the first paid invoice. A customer's second subscription shares the first one's clock. A trial delays the first paid invoice, so set attributionWindowDays longer than the trial, or the referral expires before the first payment.
Refunds and credit notes
A refund of a charge that paid an invoice adjusts the invoice's order. Each refund or credit note event sends the invoice's cumulative refund as of the event: the successful Stripe refunds of every payment of the invoice, plus the out-of-band amount of credit notes that are not void. Grizzlee removes the tax share the same way, so a full refund reverses the whole reward.
- A credit note's refund is a Stripe refund, so it counts once.
- Credit to the customer balance does not reverse commission. It lowers a later invoice's
amount_paidinstead. - Voiding a credit note lowers the total and restores the reward.
- Money no invoice kept, such as an overpayment Stripe moved to the customer balance, never earned. Refunds come out of it first, and the total never exceeds the amount the order recorded.
- A refund of a payment split across invoices falls on each invoice in proportion to what the payment gave it.
- A charge recorded as its own order keeps its refunds.
Example: 20% for 12 months
The program pays rateBps: 2000 with duration: { "type": "months", "months": 12 } and attributionWindowDays: 30. The plan is $50 a month plus 13% tax.
- On March 3 a visitor follows a partner's link. The site captures it and the server creates a Checkout Session with the
checkoutReference. - The first invoice is paid on March 3: total $56.50, $50.00 before tax. Grizzlee finds the session, claims the referral for
cus_A, savescus_A, and records an order of $50.00. The partner earns $10.00. The 12 months start March 3. - Renewals on April 3 through February 3 earn $10.00 each. The September 3 renewal fails, Stripe retries, and it is paid on September 8. It still earns $10.00.
- On May 10 the customer upgrades to $100 a month. The proration invoice collects $43.74, $38.71 before tax, and earns $7.74. Renewals from June 3 earn $20.00.
- On June 15 the merchant refunds $20.00 of the June invoice ($113.00, $100.00 before tax). That is $17.69 before tax, so 17.69% of the June reward is reversed.
- The March 3 renewal of the next year falls on the end date and earns nothing. The partner earned on 13 invoices: 12 monthly ones and the proration.
Shopify
Add the exact your-shop.myshopify.com domain, webhook signing secret and Admin API token. Grant order and customer read access, including read_all_orders. Subscribe to orders/paid and orders/updated.
The adapter sends Shopify's subtotal after discounts, before tax and shipping as commissionableCents. totalCents is Shopify's full order total, including shipping and tax. Partial refunds send the refunded merchandise subtotal as refundedCommissionableCents and the successful refund transactions' total as refundedTotalCents. It verifies that the order belongs to the connected shop and requires an exact customer order count across full history. This count includes earlier Shopify orders, even unpaid ones. The adapter sends newCustomer: true only when the count is 1, so prior Shopify orders need no Grizzlee import. Use the server API when your own history decides newness or you need a different reward amount.
The script tag sets the grizzlee_candidates cart attribute without other storefront code. Paste it in the theme: Online Store, then Themes, Edit code, layout/theme.liquid, just before </head>. Custom storefront code can instead populate cart note attributes grizzlee_program_id and grizzlee_referral_code. Send grizzlee_candidates as JSON when several captures or a typed code are available. The program decides referral scope. The adapter chooses the order ID from the verified provider order. Shopper-supplied notes cannot set payment amounts or purchase numbers. A paid order's browser_ip, or client_details.browser_ip, becomes a hashed IP signal for the order's customer, which the fraud rules compare. The raw address and the user agent are not stored. Orders without an identified Shopify customer need a merchant mapping. This connection is not a published Shopify App Store installation or OAuth app.
Paddle, Lemon Squeezy and Chargebee
These connections need no code beyond the receipt at checkout. Each webhook body holds the whole payment, so Grizzlee records orders from it and calls the provider's API only to total refunds. Paddle and Lemon Squeezy are merchants of record and include tax in their totals. Their commissionableCents is the total after discounts without tax. Chargebee's is the invoice's amount_paid without its tax share. Subscription renewals are orders for the same customer, and a receipt counts on the customer's first recorded order. Refunds arrive as cumulative totals and never rewrite earned ledger entries. See Paddle, Lemon Squeezy and Chargebee for setup, and Chargebee and Stripe together before connecting both.
Credits and outgoing events
const balances = await grizzlee.getWallets(partnerId);
const redemption = await grizzlee.redeemCredits({
partnerId,
orderId,
eventId: "stable-redemption-id",
amountCents: 500,
});
// Restore the cumulative amount after a cancelled or reduced redemption.
await grizzlee.restoreCredits(redemption.id, {
eventId: "stable-restoration-id",
restoredCents: 500,
});Choose the partner from your authenticated customer account. Redemption and restoration need credits:write. Use read to fetch balances. Debts caused by later refunds reduce future available funds.
Wallet balances include only rewards from programs that use the Grizzlee wallet. If your own system pays cash or gives credit, set that program's delivery mode to merchant. Confirm cash with deliverReward or a merchant payout. For credit, read getPartnerSummary({ externalId }) and confirm one ledger entry with deliverPartnerCredit(externalId, { externalReference, amountCents, currency }). Collect outstanding debt with recoverPartnerDebts(externalId, { externalReference, amountCents, currency, rewardType: "credit" }). Delivery and recovery need rewards:deliver. Summary reads need read. Grizzlee allocates both calls and keeps the per-reward history. See Partner-level credit delivery and debt recovery.
Configure outgoing HTTPS webhooks in Developers, then Webhooks. Verify the exact body bytes before parsing them.
import { verifyWebhook } from "@grizzlee/sdk/webhooks";
const body = await request.text();
const eventId = request.headers.get("Grizzlee-Event-Id")!;
if (
!verifyWebhook({
body,
eventId,
signature: request.headers.get("Grizzlee-Signature")!,
secret: process.env.GRIZZLEE_WEBHOOK_SECRET!,
})
)
return new Response("Invalid webhook signature", { status: 401 });
const event = JSON.parse(body);Process each event ID once. Deliveries can repeat after timeouts or worker restarts. Grizzlee retries failed deliveries up to eight attempts. Disabling an endpoint stops its queued deliveries.
Manage provider events
Grizzlee verifies provider signatures and saves incoming events before acknowledging them. Its worker processes payments and sends outgoing webhooks. Your backend should acknowledge a verified outgoing webhook after it has saved it for processing.
Developers, then Events, shows failures and their last error. Fix a connection before retrying. To repair a frozen event with missing customer or amount data, choose Correct event mapping and supply a valid server event. This is an authorized merchant correction that can create real rewards. It must retain the original event ID, order ID and action. The corrected mapping is retained for subsequent retries.
Sources:
- Stripe webhook signature verification
- Stripe metadata relationships
- Stripe transfer requirements
- Shopify webhook verification
- Shopify exact order counts
Link partner customer accounts
Partner IDs and payment customer IDs are separate. Set customerIds on each partner to the exact IDs the integration reports on orders. Shopify uses shopify:<shop domain>:<customer ID>. Stripe charge orders use grizzlee_customer_id metadata when present, otherwise the Stripe customer ID. Stripe invoice orders use the linked merchant customer ID from the Stripe customer mapping. A business partner can link its owner's customer ID. Do not copy externalId unless it is also that person's customer ID.
fraudChecks.selfReferral.action controls linked IDs and identity matches for orders and milestones. It defaults to block; review holds the reward, flag records the match, and off skips the self-referral check.
Several referrals at checkout
Send candidates to incentive lookups, reservations, claims and order completion. Use { type: "receipt", receipt } for each capture and { type: "code", referralCode, programId } for a typed code. The newest valid candidate wins while customer attribution is open, or for an only_order program. A qualified customer attribution keeps its partner, and candidates for that program get customer_already_referred. Invalid or expired candidates are skipped. Omit the list to use recorded customer referrals. See the API guide for the response fields.
Stripe charge metadata and Shopify note attributes can hold grizzlee_candidates, a JSON candidate list. Stripe invoice checkout collects candidates from the Checkout Session, subscription, invoice metadata and customer metadata, including client_reference_id. Legacy code metadata uses the verified order time. To preserve when a code was entered, send a code candidate with the backend's saved enteredAt in grizzlee_candidates. Malformed candidate JSON leaves the payment recorded and adds a warning. Invalid individual referrals never reject payment.
Partner-facing customer names
Send optional customerDisplayName beside customerId when creating an attribution, claiming referral candidates or attaching a code. On order completion, send it inside event. The value must contain 1 to 80 characters after trimming. Each accepted value replaces the referral's saved name, while omission keeps it. A name update does not change earned rewards.
The embedded dashboard shows the name or a translated "Referred customer" label and omits raw customer and order IDs from HTML and JSON. Merchant-authenticated APIs keep their IDs. Share only a name partners should see, and call customer erasure when deleting that customer's personal data. See Customer privacy.