Checkout+ is Loop’s order protection product for stores that integrate via the Checkout+ API at checkout (rather than native Loop checkout extensions). It lets you offer order protection to your customers at checkout — covering returns, shipping issues, or both — via a single API call.Your integration calls the Checkout+ API during checkout, receives eligibility and pricing information, and applies the protection charge using your platform’s native fee mechanism. When the customer completes the order, your integration pushes it to the Loop Order API with two Checkout+ data points attached to the order.
When is the Checkout+ API Integration the right fit?Checkout+ API integration is designed for merchants on WooCommerce, BigCommerce, or headless / custom platforms who want to offer order protection at checkout. If your store already uses Loop’s native checkout extensions for order protection, you typically don’t need this API integration.
Analyze the cart — Your integration sends the customer’s cart to POST /v1/analyze. The API evaluates eligibility rules, calculates the protection price, and returns a sessionId, the resolved coverage mode[], and chargeInstructions.
Present the offer — If eligible is true, display the protection offer to the customer using the price in chargeInstructions.amount and copy that matches the modes in mode[].
Apply the charge — If the customer accepts, apply the fee to the cart using whatever native fee mechanism your platform provides. Loop does not prescribe how the fee is stored on the platform.
Report the fee and session to Loop — Once the order is created, push it to the Loop Order API with both:
A fees[] entry for the protection charge, with fees[].accepted_offer_mode set to the mode array returned by /v1/analyze, verbatim. Loop uses this to recognize the charge as a Checkout+ acceptance and activate coverage.
A metadata entry with key: "session_id" and value set to the sessionId returned by /v1/analyze.
Note that this is distinct from the Loop Order API base (https://api.loopreturns.com/api/v1) — both are used by a complete Checkout+ integration. The cURL and code samples in this guide always show fully-qualified URLs so you can tell which service each call targets.
Checkout+ uses the same authentication as the rest of Loop’s APIs. Send your merchant API key in the X-Authorization header — see Authentication for how to create and manage keys, the full header contract, and the 401 response shape./v1/analyze requires the Cart scope. A key without Cart — including the key that authenticates your Order API integration — will be rejected with 401 unauthorized. Generate a dedicated Checkout+ key from Returns Management → Tools & integrations → Developer tools in the Loop Admin and select the Cart scope.
Use a Cart-only API key for Checkout+ — never re-use a broadly-scoped key./v1/analyze is browser-callable (see CORS), so any storefront that calls it from client-side JavaScript exposes its API key to every shopper who opens the network tab. A key scoped to Cart only keeps the blast radius minimal: a leaked credential can call /v1/analyze and nothing else — it cannot read orders, modify returns, or touch any other Loop resource. Re-using a key that also carries orders, returns, or any other scope turns a routine browser leak into a full-account incident.
The Checkout+ API is browser-callable — every response (success or error) carries the following CORS headers:
Header
Value
Access-Control-Allow-Origin
The request’s Origin (echoed verbatim), or * when no Origin was sent
Access-Control-Allow-Methods
GET, POST, OPTIONS
Access-Control-Allow-Headers
Content-Type, Authorization, X-Authorization
Access-Control-Max-Age
86400 (preflight cached for 24 hours)
Browsers may issue an OPTIONS preflight before the actual request whenever you send X-Authorization or a non-simple Content-Type. Loop responds to the preflight with the headers above and a 204 No Content, no merchant API key required for the OPTIONS itself.
Calling /v1/analyze directly from the browser exposes your merchant API key to anyone who inspects the network tab. Keep the credential server-side and proxy through your own backend whenever possible.
Send the customer’s cart data to POST /v1/analyze whenever the cart changes or the customer reaches checkout.At minimum, include:
platform — woocommerce, bigcommerce, custom, or another string identifying your storefront.
region — the geographic region for eligibility evaluation. Either an ISO 3166-1 alpha-2 country (e.g. "US"), an ISO 3166-2 region ("US-OH"), or a structured { country, subdivision? } object.
cart — with lines, currencyCode, subtotalAmount, totalDiscountAmount, discountedSubtotalAmount, totalTaxAmount, totalAmount, and itemCount.
Each cart line requires id, productId, quantity, totalPrice, originalTotalPrice, and title. unitPrice is optional.Optionally include a top-level cartId (a stable identifier from your platform) or sessionId (returned by a previous /analyze call) to re-use the existing offer session instead of creating a new one. See Idempotency.
Stable identifier for this offer session. Persist it alongside your cart so subsequent /analyze calls can re-use the same session.
eligible
Whether to show the protection offer.
reason
Why the cart is or isn’t eligible (useful for debugging).
mode
A subset of ["returnCoverage", "shippingProtection"] describing what coverage was offered. Present only when eligible is true — omitted when the cart is ineligible. Drives the customer-facing copy and is reported back on the order as fees[].accepted_offer_mode.
chargeInstructions.amount
The fee to charge, in minor currency units.
chargeInstructions.method
fee for Checkout+ API integrations. (product_line_item is reserved for Loop’s Shopify integration.)
metadata.priceBreakdown
Component-by-component breakdown of how the protection price was calculated (one entry per coverage mode). Useful for debug surfaces and support tooling.
metadata.sessionCreated
true if this call created a new session, false if it re-analyzed an existing one.
metadata.analysisCount
How many analyses this session has had, including the current one.
If eligible is false, chargeInstructions is null — do not show the offer.
3
Apply the charge
If the customer accepts protection, add a fee to the order using whatever native mechanism your platform provides. Loop does not prescribe the storage format — only the amount and currency need to match chargeInstructions.
BigCommerce — the Checkout Fees API (or an order-level fee on the Orders API).
Headless / custom — any order-level fee or surcharge supported by your commerce platform.
Convert chargeInstructions.amount from minor currency units (e.g. 498 → 4.98) when your platform expects major units.
4
Report the fee and session to Loop
Once the order is created on your platform, push it to the Loop Order API (or upsert it via your existing order-sync pipeline). The order payload must include both:
A fees[] entry for the protection charge, with accepted_offer_mode set to the mode array returned by /v1/analyze, verbatim — do not transform, sort, or stringify the array.
A metadata entry with key: "session_id" and value set to the sessionId returned by /v1/analyze (e.g. lop_a1b2c3d4e5f6g7h8i9j0).
The fragment below highlights only the two Checkout+-specific fields on the Create Order payload — the rest of the order shape (line items, addresses, totals, customer, etc.) is unchanged. See the Create Order reference for the full schema, headers, and authentication.
{ "...": "standard Create Order fields — see the Create Order reference", "fees": [ { "name": "Order protection", "amount": { "amount": 498, "currency_code": "USD" }, "accepted_offer_mode": ["returnCoverage", "shippingProtection"] } ], "metadata": [ { "key": "session_id", "value": "lop_a1b2c3d4e5f6g7h8i9j0" } ]}
The accepted_offer_mode array tells Loop which coverage modes the customer paid for:
accepted_offer_mode
Coverage activated
["returnCoverage"]
Return coverage only
["shippingProtection"]
Shipping protection only
["returnCoverage", "shippingProtection"]
Both return coverage and shipping protection
The session_id metadata entry tells Loop which offer session this acceptance corresponds to.
Your checkout UI controls how the protection offer is presented to customers. Loop does not inject any UI — your integration is responsible for rendering the offer, handling customer interaction, and reflecting the charge in the cart total.
Display the protection offer after the cart summary and before payment. The offer should feel like a natural part of the checkout flow, not a popup or interstitial.Recommended placements:
Below the order summary / line items
Above the payment method section
Near shipping options (especially when mode includes shippingProtection)
Place the offer where customers are already reviewing costs. This context makes the price feel incremental rather than unexpected.
Format the price for display by converting from minor units (e.g., 498 → $4.98). Use currencyCode to apply the correct currency symbol and formatting for the customer’s locale.
Some jurisdictions have regulations around pre-selected charges at checkout. Verify that your default state complies with applicable laws (e.g., EU Consumer Rights Directive, FTC guidelines).
The mode array in the response tells you what type of protection is being offered. Use it to tailor the description shown to the customer.
mode contains
What it covers
Suggested description
["returnCoverage"]
Returns
”Get hassle-free returns on this order”
["shippingProtection"]
Lost, stolen, or damaged packages
”Protect your package against loss or damage during shipping”
["returnCoverage", "shippingProtection"]
Returns + shipping issues
”Cover your order for easy returns and shipping protection”
These are suggestions — use language that fits your brand voice. The important thing is that the description accurately reflects every mode in the array.
Your UI should handle the time between calling the API and receiving a response, as well as failures:
State
Recommendation
Loading
Hide the offer or show a skeleton/placeholder until the API responds. Do not show stale pricing from a previous cart state.
Error / timeout
Hide the offer entirely. Do not block checkout if the Checkout+ API is unavailable.
Ineligible (eligible: false)
Hide the offer. Do not show a disabled or grayed-out toggle.
Checkout+ should never block the checkout flow. If the API call fails, times out, or returns an error, proceed with checkout normally — without the protection offer.
Re-call POST /v1/analyze whenever the cart contents change (items added, removed, or quantities updated). Pass the same cartId (or sessionId from the first response) so you re-use the existing offer session.When the cart changes:
Call /v1/analyze with the updated cart data and the same cartId / sessionId.
Update the displayed price with the new chargeInstructions.amount.
Preserve the customer’s opt-in/opt-out selection across updates.
If a previously eligible cart becomes ineligible after a change (e.g. an excluded product is added), the next /analyze response will return eligible: false and chargeInstructions: null. Hide the offer immediately and reset any stored opt-in state — you must not apply a fee against a stale chargeInstructions.
The mode array can change between /analyze calls as the cart changes (for example, removing a non-shippable item may drop shippingProtection from the array, leaving only ["returnCoverage"]). When you detect a change in array membership, re-render the coverage description and price so the customer sees what they’re actually being offered, then re-confirm their selection if the change is material.
There is no explicit acceptance or rejection API call. Loop determines acceptance from the order payload your integration sends to the Loop Order API:
Outcome
How Loop determines it
Accepted
The order arrives with a fees[] entry whose accepted_offer_mode is set, and a session_id metadata entry. Coverage is activated and the order is billed.
Declined
The order arrives with no protection fee (no fees[] entry carrying accepted_offer_mode). No coverage, no billing.
Not shown
/v1/analyze returned eligible: false. No offer was presented.
Abandoned
The cart was analyzed but no order was placed. No action needed.
This approach is more reliable than a label-matching heuristic because the fee classifies the acceptance (accepted_offer_mode) and the metadata pairs that acceptance with the exact offer session that priced it (session_id).
Every /v1/analyze response carries a reason that explains the eligibility outcome. When eligible is true, reason is always eligible. When eligible is false, reason describes the disqualifying condition — use it to debug configuration issues, understand offer-attachment gaps, and drive any merchant-facing debug tooling you build on top of Checkout+.
reason
Meaning
What to do
eligible
Cart is eligible; mode[] and chargeInstructions are populated.
Show the offer.
configuration_missing
No Checkout+ configuration exists for this merchant.
Contact Loop support — this is a server-side fix, not a merchant request issue.
order_protection_disabled
Checkout+ is disabled for this merchant.
Re-enable from the Loop admin or contact support.
empty_cart
Cart has no line items.
Hide the offer. Don’t call /v1/analyze until the cart has at least one line.
cart_only_has_coverage_products
Cart contains only Loop coverage products — nothing to protect.
Hide the offer.
cart_only_has_excluded_products
Every item in the cart matches an exclusion rule.
Hide the offer.
cart_has_excluded_product
At least one item matches an exclusion rule and the merchant’s configuration treats that as disqualifying.
Hide the offer.
b2b_customer_excluded
customer.isB2B is true and the merchant excludes B2B carts.
Hide the offer. Only occurs when you pass the optional customer block.
pickup_delivery_selected
A deliveryGroups[].deliveryMethod is PICKUP or LOCAL_DELIVERY.
Hide the offer. Only occurs when you pass deliveryGroups.
cart_total_too_low
Cart subtotal is below the minimum threshold configured for this merchant.
Hide the offer. May become eligible after more items are added.
cart_too_valuable
Cart subtotal exceeds the maximum coverage amount.
Hide the offer.
locale_not_supported
The customer’s locale is not in the merchant’s supported locales.
Hide the offer.
currency_not_supported
cart.currencyCode is not in the merchant’s supported currencies.
Hide the offer.
ab_test_affects_attachment_rate_control_group
Session was assigned to an experiment control group that suppresses the offer.
Hide the offer. Expected when you opt in to experimentation.
rule_evaluation_error
A merchant-configured eligibility rule threw during evaluation.
Hide the offer — do not block checkout. Report the X-Request-Id to Loop support if the rate is non-trivial.
system_error
Loop encountered an unexpected error while evaluating eligibility.
Hide the offer — do not block checkout. Safe to retry the next time the cart changes.
For every ineligible reason, chargeInstructions is null and mode is omitted from the response. Treat any reason other than eligible as “hide the offer” — never present a disabled / grayed-out toggle.
When the cart is eligible, the response includes a chargeInstructions object that tells your integration how to apply the protection charge.
Field
Description
amount
The fee amount in minor currency units (e.g., 498 = $4.98 USD).
currencyCode
ISO 4217 currency code for the charge.
method
The charge mechanism. fee for Checkout+ API integrations on WooCommerce, BigCommerce, and headless storefronts. product_line_item is reserved for Loop’s Shopify integration where the charge is added as a hidden coverage product.
product
Only present when method is product_line_item. Contains id and handle for the Shopify coverage product.
When the cart is ineligible, chargeInstructions is null.
When your merchant configuration includes shippingProtection, pass deliveryGroups on every /v1/analyze call so Loop can detect pickup or local-delivery selections that disqualify the cart from shipping coverage. If you omit deliveryGroups, Loop assumes the order is being shipped and will offer shipping protection on in-store pickup orders.Each entry in deliveryGroups carries a deliveryMethod:
deliveryMethod
Meaning
Effect on shipping coverage
SHIPPING
Standard shipped delivery
Eligible
PICKUP
In-store or curbside pickup
Disqualifies (reason: pickup_delivery_selected)
LOCAL_DELIVERY
Merchant-operated local delivery
Disqualifies (reason: pickup_delivery_selected)
DIGITAL
Digital / no physical fulfillment
Eligible, but typically no shipping exposure
Map your platform’s native delivery types to these generic values before sending — for example WooCommerce’s local_pickup → PICKUP, BigCommerce store pickup → PICKUP.
Merchants whose protection mode is returnCoverage only do not need to send deliveryGroups — return coverage doesn’t depend on how the order is fulfilled.
GET /v1/session/{sessionId} returns the current state of an offer session without re-analyzing the cart. Use it from any context where you have the sessionId but don’t have (or don’t want to re-send) the cart payload — for example a debug surface, an admin tool, or a downstream worker auditing offer outcomes.
The response is the same shape as POST /v1/analyze — sessionId, eligible, reason, mode, chargeInstructions, metadata — with two differences:
Field
Behavior
createdAt / updatedAt
Present on GET, not on POST /v1/analyze. RFC 3339 / ATOM timestamps without milliseconds (e.g. 2026-04-23T13:25:00+00:00) that bound the session’s lifetime.
metadata.sessionCreated
Omitted on GET (it only describes the analyze call that created or re-used the session, not the session itself).
Sessions are merchant-scoped. A session created under one merchant API key is invisible to others — you’ll get 404 session_not_found from the wrong key.
Responses are sent with Cache-Control: private, max-age=45. A short shared cache is fine for poll loops; do not cache across merchants or longer than 45 seconds.
This endpoint is not the right way to drive checkout UI updates — always re-call POST /v1/analyze when the cart contents change so pricing reflects the latest state.
You can re-use an existing offer session in two ways:
Method
How
When to use
sessionId
Pass the sessionId from a previous response at the top level of the request
You already have a session and want to re-analyze with updated cart data
cartId
Pass a stable cart identifier from your platform at the top level of the request
Your platform provides a consistent cart ID across checkout (e.g. BigCommerce’s cart_id) and you’d rather not persist sessionId separately
Either approach updates the existing analysis rather than creating a new one. The response will reflect the new pricing for the updated cart, and metadata.sessionCreated will be false with metadata.analysisCount incremented.
The format is fixed: ^lop_[A-Za-z0-9]{20}$ (e.g. lop_a1b2c3d4e5f6g7h8i9j0).
A request with a malformed sessionId fails with 400 invalid_request. Omit the field to let Loop generate a new session.
A well-formed sessionId that does not exist for your merchant fails with 404 session_not_found. This usually means the session was never created under this API key, was created against a different environment, or has been evicted.
When both sessionId and cartId are sent, sessionId takes precedence — Loop looks up the session directly and ignores cartId for session selection.
These are two different fields with different roles — easy to confuse:
Field
Where
Role
cartId
Top-level on the analyze request
Merchant-scoped session key. Loop indexes the cart against the offer session; subsequent /analyze calls with the same cartId re-use the same session.
cart.id
Inside the cart object
On-platform identifier, echoed verbatim and surfaced in analytics / debug output. Has no idempotency role — sending only cart.id will not re-use a previous session.
If you want idempotent re-analysis, you must send the value at the top level as cartId (or pass sessionId).
BigCommerce provides a stable cart_id automatically. For WooCommerce and headless platforms, use whatever stable cart identifier your platform provides, or pass back the sessionId from the initial analysis.
Set debug: true on the request to receive a debug block on the response with eligibility, pricing, and configuration diagnostics. Use this only during integration testing — the field is gated behind the request flag and is not intended for production use.Always include the X-Request-Id from the response header when reporting issues to Loop support. It is the fastest way for us to pull the full request/response trace from our logs.
/v1/analyze accepts a handful of optional fields that do not influence pricing or eligibility but help Loop segment attach-rate reporting and run A/B tests on your behalf. They’re safe to omit — pass them when you want the analytics value.
Field
Type
Purpose
experimentationConsent
boolean
Opt the session in to A/B test cohort assignment. When true, Loop may assign a cohort and return it in metadata.experimentCohort; when false or omitted, metadata.experimentCohort is always null. Required if you want this session to participate in Loop-run experiments.
offerOrigin
string
Where the offer is being presented — e.g. "checkout", "cart". Defaults to "custom" when omitted. Analytics only.
mountTarget
string
UI mount location — e.g. "below_shipping", "order_summary". Defaults to "custom" when omitted. Analytics only.
cartExperience
string
UI experience variant identifier — e.g. "widget", "modal". Defaults to "api" when omitted. Analytics only.
If a session is assigned to an experiment control group that suppresses the offer, /v1/analyze will return eligible: false with reason: ab_test_affects_attachment_rate_control_group. Treat this the same as any other ineligible outcome — hide the offer.
All amounts on /v1/analyze are in minor currency units (cents). $10.00 = 1000. The Order API uses major-unit decimal strings — convert when you push the fee.
Currency codes
ISO 4217 format (e.g., USD, EUR, GBP).
Cart updates
Re-call /v1/analyze whenever the cart changes to get updated pricing, passing the same cartId or sessionId.
Mode mapping
Pass mode[] through to fees[].accepted_offer_mode verbatim — do not collapse, sort, or transform the array.
Session linkage
Pass the sessionId from /v1/analyze through to the order as a metadata entry with key: "session_id". Use the key name exactly.
Request body size
The /v1/analyze endpoint enforces a 1 MB request body limit. Larger payloads are rejected with 400 invalid_request (“Request body too large”). Trim oversized cart attributes (e.g. raw HTML or base64-encoded blobs) before sending.
Additive changes
Loop may add new fields to API responses at any time. Build your integration to tolerate unknown fields.
Full request and response schema for the POST /v1/analyze endpoint.
Create Order — API Reference
Where to push the order with fees[].accepted_offer_mode and the session_id metadata entry so Loop can activate Checkout+ coverage and link the order to its offer session.
Loop Anywhere
Push commerce data into Loop from any source — orders, products, customers, and more.
⌘I
Assistant
Responses are generated using AI and may contain mistakes.