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.
The elements below are required, not optional. Loop’s attach-rate guarantees and the conversion commitments in your Checkout+ agreement assume the offer is presented exactly as specified — a clear coverage value proposition and a primary/secondary action pair. Integrations that omit these elements, bury the offer, or de-emphasize the accept action consistently under-perform and put contractual adoption targets at risk. Treat this section as the minimum bar for a compliant Checkout+ surface.
The screenshots below show a compliant Checkout+ surface rendered in a cart drawer. Build to match this layout and emphasis.
The offer surface above includes every required element:
Element
Requirement
Coverage line + price
A clearly labeled coverage line (“Checkout+ with package protection”) with the price from chargeInstructions, shown alongside the cart total.
Value proposition
A short benefit-oriented description (“Avoid return handling fees and get coverage for items lost or damaged in transit”).
Primary accept action
The Checkout+ button that proceeds with coverage, rendered in the theme’s primary color. Label it clearly (e.g. “Checkout+ | $754.95” with the cart total including the fee).
Secondary decline action
A “Continue without coverage” button rendered in the theme’s secondary color (de-emphasized / outlined) that proceeds without the fee.
Do not invert button emphasis. The accept action must use the theme’s primary color; the decline action must use the secondary color. Styling both actions identically, or putting the decline action in the primary color, measurably depresses attach rate and violates the presentation expectations in your Checkout+ agreement.
Optional: An info (ⓘ) control that opens a modal with fuller coverage details is a supported pattern but not required. If you include it, the modal content should accurately reflect the modes in mode[] — see the example below.
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.
Place the offer where customers are already reviewing costs. This context makes the price feel incremental rather than unexpected.
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 disabled or grayed-out accept/decline buttons.
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 coverage-line price, accept-button total, and any other displayed amounts from the new chargeInstructions.
If the customer already chose accept or decline, preserve that choice across updates unless eligibility or mode changes make it invalid (see below).
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 clear any stored accept selection — 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 array membership changes, re-render the value proposition, coverage-line label, and button prices so the customer sees what they’re actually being offered. If the change is material, ask them to choose again rather than silently keeping a prior accept.
Follow standard accessibility practices for the offer UI:
Use native <button> elements (or ARIA-equivalent controls) for the accept and decline actions, with clear accessible names that include the outcome (e.g. “Checkout with protection, $754.95” / “Continue without coverage”).
Ensure both actions are keyboard-focusable and operable without a pointer.
Maintain sufficient color contrast for button labels, the value proposition, and price text — including when using theme primary/secondary colors.
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).
When a customer cancels an order that accepted Checkout+ protection on your platform, you must sync that cancellation back to Loop so coverage and billing can be reversed. Push an update via the Update Order or Upsert Order endpoint — the same order-sync pipeline you use for fulfillments and refunds.
Both fields are required. Include status: "cancelled"andcancelled_at on the order payload. Sending status alone is not sufficient — Loop uses the timestamp to reverse self-managed package protection.
Field
Requirement
status
Set to "cancelled".
cancelled_at
RFC 3339 / ATOM timestamp of when the order was cancelled, without milliseconds (e.g. 2026-06-05T14:30:00+00:00).
Subscribe to your platform’s order-cancellation events (or include cancellation in your reconciliation job) so protected orders are updated in Loop promptly after they are cancelled on your side.
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.
Order cancellation
When a protected order is cancelled on your platform, update it in Loop with status: "cancelled" and cancelled_at so protection coverage is reversed. See Cancelling Protected Orders.
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.