Build with the noli loyalty API
Awarding points, redeeming rewards, and minting coupons — all over plain JSON. No SDK. Any backend.
Overview
Use the Custom API integration when your store runs on a platform we don't natively support yet (headless commerce, custom backends, ERP systems, etc.). You'll do two things:
- Send orders to Noli from your backend so we can award points to the matching customer.
- (Optional) Expose a promo-code endpoint we can call when a customer redeems a reward — so the code already exists in your e-commerce platform.
Both flows are JSON over HTTPS. There's no SDK — any language with an HTTP client works.
Authentication
Every request to the noli API is identified by your public API key, sent as the x-api-key header.
You can find your key in Settings → Integrations → Custom API on the admin panel. Treat it like a password — don't commit it to git, don't ship it to browsers, don't paste it into chat. If it leaks, regenerate from the same screen.
x-api-key: pk_live_••••••••••••••••••••••••••••••Note: requests are also identified by business_id in the body — that's the canonical tenant identifier. The API key prevents requests from other merchants reaching your business by accident.
Earn points
Your backend → Noli
https://qremotwvbhxqkzmxrqzs.supabase.co/functions/v1/public-add-pointsCall this once per completed order. Noli looks up the customer (by phone or email), applies your earn rules, and awards points. If the customer isn't registered yet, the order is held as pending and points are awarded automatically when they sign up.
Requests are idempotent on order_id — sending the sameorder_id twice is safe and returns the original result.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| business_id | string (uuid) | required | Your business ID. Visible in the admin panel URL. |
| order_id | string | required | Your internal order identifier. Used for idempotency. |
| amount | number | required | Order subtotal in your store currency. |
| customer_email | string | optional | Customer email. Provide either email or phone (or both). |
| customer_phone | string | optional | Customer phone in E.164 format. |
| order_items | array | optional | Optional line items — used for catalog analytics. |
Order item fields
Optional, but recommended. Each entry in order_items takes the shape:
| Field | Type | Required | Description |
|---|---|---|---|
| product_sku | string | optional | Stock keeping unit. Helps catalog matching. |
| product_id | string | optional | Your internal product ID. |
| product_name | string | required | Display name of the product. |
| quantity | number | required | Units purchased. |
| unit_price | number | required | Per-unit price in your store currency. |
Response shapes
Four shapes you'll see — all return HTTP 200 except validation errors (4xx / 5xx).
- Awarded — points went on the customer's balance.
- Pending — customer not registered yet, order parked.
- Duplicate — same
order_idalready processed (idempotency). - Error — validation or server failure.
Code examples
Drop-in clients for the most common backends.
Thank-you widget
Drop-in client snippet for the order-confirmation page
The widget is a small JS embed you place on your order-confirmation / thank-you page. It posts the order to noli and renders a "Claim your points" CTA (with QR code for mobile handoff) that takes the customer to your customer portal. The portal handles registration with Viber, email, or phone as options, and credits the points on success. Same idempotency guarantee as the server-to-server endpoint — duplicate order_id is safe.
Use the widget when:
- You want a one-tap claim CTA without writing the UI yourself.
- Your platform doesn't expose a server-side webhook for order completion.
- You want the merchant's brand colors applied automatically.
Prefer the server-to-server public-add-points endpoint if you don't need a customer-facing CTA or need to award points outside the customer's session (e.g. async fulfilment).
Endpoint
https://qremotwvbhxqkzmxrqzs.supabase.co/functions/v1/create-pending-transaction-and-redirectThe widget posts here under the hood. The primary CTA target is portal_redirect_url — the merchant's loyalty portal with the order encoded as a query param. The portal then handles the claim (already-awarded vs needs-registration vs duplicate) and offers Viber, email, or phone as registration options when needed.
Three states are possible:
- Awarded — points already credited (Viber subscriber). Response includes
points_addedandnew_balance. - Pending — order parked.
registration_required: true; the portal will award points after the customer registers. - Duplicate — same
order_idalready processed;already_processed: true.
Note: viber_redirect_url is also returned but is deprecated — it points to a raw viber:// deep link, which is what widget v1 used. New integrations should use portal_redirect_url so the customer can pick a registration channel instead of being forced into Viber.
Embed snippet
Drop this into your thank-you page template. The script readswindow.NoliLoyalty for configuration, so you can inject merge tags from your platform (Liquid, Twig, ERB, etc.).
| Field | Type | Required | Description |
|---|---|---|---|
| business_id | string (uuid) | required | Your business ID — visible in the admin panel URL. |
| order_id | string | required | Your internal order id. Used for idempotency. |
| amount | number | required | Order subtotal in your store currency. |
| customer_email | string | optional | Customer email. Pass either email or phone (or both). |
| customer_phone | string | optional | Customer phone in E.164 format. |
| brand_primary | string (hex) | optional | Primary button color. Defaults to your business setting. |
| brand_secondary | string (hex) | optional | Hover / gradient end color. Defaults to your business setting. |
Brand colors
The widget uses the same Primary + Secondary colors you picked in Branding step during onboarding (or set later under Settings → Branding). When you don't pass brand_primary orbrand_secondary in the snippet, the script fetches them from your business profile so the button stays in sync if you rebrand.
If you do want to override (e.g. seasonal campaign), pass hex values in window.NoliLoyalty:
window.NoliLoyalty = {
// …
brand_primary: '#FF7A6B', // overrides for this page only
brand_secondary: '#C866B8',
};The script renders a 135° linear gradient from primary to secondary, matching the loyalty card on the customer portal.
Per-platform tips
- Shopify — paste the snippet into Settings → Checkout → Order status page → Additional scripts. Liquid merge tags (
{{ order.id }},{{ customer.email }}) are evaluated server-side. - WooCommerce — hook into
woocommerce_thankyouin your theme'sfunctions.php(or via a snippets plugin) and inject the script tag with PHP-rendered values. - BigCommerce / OpenCart / custom — render the snippet into the order success template and substitute the order id, total and customer fields with the platform's variables.
- SPA / headless — call the endpoint manually (see "Manual fetch" tab) and render your own button using the response.
Promo code generator endpoint
Noli → your backend
When a customer redeems a reward, Noli normally generates a code internally and shows it to the customer. If you'd rather mint the code in your own system (so the customer gets a code that already exists in your e-commerce platform, ERP, or POS), expose an endpoint and we'll call it on each redemption.
Configure the endpoint URL, HTTP method, and parameter mapping under Settings → Rewards → Promo code method → Custom API on each reward.
What Noli sends
HTTP method: GET or POST (you choose). Headers include Content-Type: application/json on POST.
Static params: any key/value pairs you configured — useful for an authentication token your endpoint requires.
Dynamic params: resolved from the redemption context. Available paths:
| Field | Type | Required | Description |
|---|---|---|---|
| user.id | string | optional | Loyalty member id. |
| user.name | string | optional | Member name (if known). |
| user.email | string | optional | Member email (if known). |
| user.phone | string | optional | Member phone E.164 (if known). |
| user.points_balance | number | optional | Member balance after redemption. |
| reward.id | string | optional | Reward id being redeemed. |
| reward.name | string | optional | Reward name. |
| reward.cost_points | number | optional | How many points the customer spent. |
| reward.discount_value | number | optional | Percent or fixed amount (ignored for free_shipping / free_item). |
| reward.reward_type | string | optional | "discount_percent", "discount_fixed", "free_shipping", "free_item" or "tier_unlock". |
| reward.discount_type | string | optional | Normalised flag: "percent" | "fixed_eur" | "free_shipping" | null. |
| reward.is_percent | string | optional | "1" when percent discount, else "0". |
| reward.is_fixed | string | optional | "1" when fixed-amount discount, else "0". |
| reward.is_free_shipping | string | optional | "1" for a free-shipping reward (map to a free-shipping coupon; amount is 0/irrelevant), else "0". |
What you return
Respond with JSON. Noli reads two paths:
response_code_path— the path to the generated code (defaultcode).response_success_path— optional. If set, must resolve to a truthy value for the redemption to succeed.
If your endpoint returns a non-2xx status or the response doesn't contain a code at the expected path, redemption fails and the customer's points are not spent — so transient errors are safe.
Example endpoint
A minimal Express handler that mints a coupon in your e-commerce platform.
Need help?
Reach out at support@heynoli.ai with your business_id and a sample request — we'll get back the same day.
Also see: How loyalty rules work