Webshop integration
This guide covers the end-to-end flow for offering RE-ZIP packaging in your own webshop: how to find the packaging SKUs available to your shop, how to record a packaging order when a customer chooses reusable packaging, and how to react to order state changes.
It is written for teams building their own storefront — a headless frontend (Shopify Hydrogen, Next.js, Nuxt), a custom checkout, or a platform we don’t ship a plugin for. If you run a standard Shopify Liquid theme you don’t need any of this: our Shopify app implements the whole flow for you. Email helpdesk@re-zip.com and we’ll get you set up with it.
Prerequisites
Section titled “Prerequisites”- A RE-ZIP shop account with an access token or PAT (see Getting started)
- Your shop’s ULID, referred to below as
$SHOP_ID - A publicly reachable HTTPS endpoint, if you want order webhooks (see Webhooks)
Throughout this guide $TOKEN is your token. Orders accept Bearer and Pat
authorization; for a server-side integration a scoped PAT is the better choice.
Develop against https://api.sandbox.re-zip.com and switch the host to
https://api.re-zip.com when you go live. Every request needs an
Accept-Version header; the current production version is 2.0.
How it works
Section titled “How it works”Customer adds RE-ZIP packaging to their basket │ ▼Your checkout completes the order │ ▼Your server POSTs the packaging lines to /shops/$SHOP_ID/orders │ ▼RE-ZIP validates the SKUs against your assortment and creates the order │ ▼RE-ZIP fulfils the packaging; order.updated fires as the state changes │ ▼Customer returns the packaging at a drop point and earns a voucherYour storefront owns the customer-facing half: showing the option, pricing it, and carrying the choice through checkout. RE-ZIP owns the packaging half: the assortment, fulfilment, the return network, and the reward.
Step 1: Find your SKUs
Section titled “Step 1: Find your SKUs”Every shop has an assortment — the set of packaging products it may order. An order is rejected if it references a SKU outside that set, so read the assortment first rather than hardcoding SKUs.
$ curl -s \ -H 'Accept-Version: 2.0' \ -H 'Authorization: Bearer $TOKEN' \ https://api.re-zip.com/shops/$SHOP_ID/products | pp.json[ { "id": "01JAYSCS07SVDQANFS61TF0XJN", "sku": "REZIP-BAG-M", "name": "RE-ZIP Bag Medium", "price": 1200, "active": true, "currency": "DKK", "dimension": { "width": 250, "height": 80, "depth": 350 }, "weight": 95, "volume": 7000, "surface_area": 1750, "primary_material": "rPET", "bundle_size": 50, "group": { "name": "Bags", "description": "Reusable mailer bags" }, "created_at": "2026-04-22T10:00:00.000Z", "updated_at": "2026-04-22T10:00:00.000Z", "deleted_at": null }]Notes on the fields that matter for a storefront:
| Field | Use |
|---|---|
sku | The identifier you send when creating an order. The only field the order endpoint reads. |
name, group.name | Safe to show to customers |
price | The price RE-ZIP charges you, in minor units. Not a retail price — see below. |
currency | The currency price is denominated in, resolved from your shop’s ordering settings |
active | Filter out anything not true |
dimension, weight, volume | For a size picker, or to match packaging to basket contents. dimension is {width, height, depth} in millimetres, any of which may be null. |
The Shops scope reference lists every field the endpoint returns.
price is your cost, not what you charge your customer. Whether you pass it on,
absorb it, or charge something else entirely is your decision, and it lives in
your storefront and your own product catalogue — RE-ZIP never sees your retail
price.
Cache the assortment rather than fetching it per pageview. It changes when your agreement changes, which is rare.
Step 2: Offer packaging in your storefront
Section titled “Step 2: Offer packaging in your storefront”This step is entirely yours, and it varies by platform, so the guide only states the contract: by the time an order is placed, your backend needs to know which SKU the customer chose and how many units.
How you carry that through checkout is up to you. Two common shapes:
- A product in your own catalogue. Create a product (or variant per SKU) in your commerce platform, mapped to a RE-ZIP SKU. The line item flows through checkout, your fulfilment, and your accounting like any other product. This is what the Shopify app does, and it’s the shape to copy if your logistics provider needs to see the packaging as a sellable line.
- A cart attribute or order metadata field. Lighter weight, but invisible to your fulfilment pipeline unless you propagate it yourself.
If you are on a headless storefront, note that you cannot reuse the Shopify app’s widget: it is a theme app extension and depends on Liquid and the Ajax Cart API. Build the opt-in into your own components and mutate the cart with your platform’s own API.
Step 3: Create the packaging order
Section titled “Step 3: Create the packaging order”When the customer’s order is placed, POST the packaging lines to RE-ZIP. Do this server-side — it needs your token, which must never reach the browser.
$ curl -s -X POST \ -H 'Accept-Version: 2.0' \ -H 'Authorization: Bearer $TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "currency": "DKK", "lines": [ { "sku": "REZIP-BAG-M", "quantity": 2 }, { "sku": "REZIP-BOX-S", "quantity": 1 } ] }' \ https://api.re-zip.com/shops/$SHOP_ID/orders | pp.json{ "id": "01JAYSDB1037Y6X8WSJ1EZQ03X", "number": 1042, "state": 0, "type": "normal", "aggregate_price_at_creation": 3600, "lines": [ { "id": "01JAYSDB1037Y6X8WSJ1EZQ040", "sku": "REZIP-BAG-M", "name": "RE-ZIP Bag Medium", "dimensions": "W: 250.0mm, H: 80.0mm, D: 350.0mm", "price": 1200, "quantity": 2, "fulfilled": 0, "created_at": "2026-04-22T10:00:00.000Z", "updated_at": "2026-04-22T10:00:00.000Z" }, { "id": "01JAYSDB1037Y6X8WSJ1EZQ041", "sku": "REZIP-BOX-S", "name": "RE-ZIP Box Small", "dimensions": "W: 200.0mm, H: 100.0mm, D: 300.0mm", "price": 1200, "quantity": 1, "fulfilled": 0, "created_at": "2026-04-22T10:00:00.000Z", "updated_at": "2026-04-22T10:00:00.000Z" } ], "created_at": "2026-04-22T10:00:00.000Z", "updated_at": "2026-04-22T10:00:00.000Z"}Request body
Section titled “Request body”| Field | Type | Description |
|---|---|---|
currency | string | ISO 4217 code the order is denominated in (e.g. "DKK", "EUR") |
lines | array | One entry per packaging SKU |
lines[].sku | string | A SKU from your assortment (Step 1) |
lines[].quantity | number | How many units |
name, dimensions, and price on each response line are resolved from your
assortment at creation time — you do not send them, and sending them has no
effect. aggregate_price_at_creation is the order total in minor units, frozen
at creation so a later assortment price change doesn’t rewrite history.
A newly created order has state: 0, meaning open. It leaves that state as
RE-ZIP fulfils it; subscribe to order.updated (Step 4) rather than polling.
Errors
Section titled “Errors”| Status | Cause |
|---|---|
400 | A SKU is not in your assortment. The message names it: SKU not in assortment: <sku>. |
404 | $SHOP_ID is not a shop, or is not linked to a partner |
401 | Missing or invalid token |
A 400 here is a configuration problem, not a transient one — retrying will not
fix it. Log it, alert on it, and fall back to completing the customer’s order
without RE-ZIP packaging rather than failing their checkout.
Retries and duplicate orders
Section titled “Retries and duplicate orders”The endpoint has no idempotency key. Two identical POSTs create two orders. Since a retry after a network timeout cannot tell whether the first attempt landed, protect against duplicates on your side:
- Record the returned order
idagainst your own order before you consider the step done, and skip the call if one is already recorded - Drive the call from a queue or job with at-most-once semantics, not from the request that completes checkout
- If you do end up unsure,
GET /shops/$SHOP_ID/ordersand reconcile bycreated_atbefore retrying
Step 4: Subscribe to order webhooks
Section titled “Step 4: Subscribe to order webhooks”Rather than polling for fulfilment, subscribe to order events. Two events are
available: order.created and order.updated.
$ curl -s -X POST \ -H 'Accept-Version: 2.0' \ -H 'Authorization: Bearer $TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "event": "order.updated", "url": "https://your-server.example.com/rezip/order-updated" }' \ https://api.re-zip.com/shops/$SHOP_ID/webhooksAn unknown event name returns 400. Deliveries are signed with
HTTP Message Signatures (RFC 9421) —
see Webhooks for verification and retry behaviour. Verify
the signature before acting on the payload.
order.created fires for orders you created yourself too, so if you already
record the order id from the Step 3 response, order.updated is usually the
only subscription you need.
Step 5: Reward returning customers
Section titled “Step 5: Reward returning customers”When a customer drops packaging at a drop point, RE-ZIP issues them a voucher for your shop. There are two ways to supply those vouchers: upload a static pool of codes up front, or register an on-demand pool, where RE-ZIP calls your server to mint one at the moment it’s earned.
If you can run a webhook endpoint, use an on-demand pool. You are already building the integration in this guide, so you have the one prerequisite, and it is the better option on every axis that matters:
- You cannot run out. A static pool is inventory: finite codes that deplete as customers redeem them. An on-demand pool has nothing to deplete, because the code is generated per request.
- Running dry is silent. When a static pool is exhausted, nothing errors. The reward falls down an internal chain to your configured default voucher, and then to a generic ticket. Your customer gets something less than you intended, you are not told, and the first signal is usually a support email.
- It’s set-up-and-forget. The generation logic lives in your codebase, under version control, reviewed like the rest of your code — rather than as a batch of codes somebody has to remember to top up. You configure the pool once and move on.
- The code can be real at the moment it’s earned. Mint it in your own
commerce platform inside the webhook handler and return a
linkthat drops the customer into a basket with the discount already applied, instead of handing them a code to copy.
Uptime is not a concern: if your endpoint is unreachable or errors, RE-ZIP falls back to any static pool you have. So a small static pool is a reasonable safety net underneath an on-demand one — not a replacement for it.
That flow has its own guide: Voucher integration. Read Vouchers first if you want to know how each field is rendered to the customer.
Generating a typed client
Section titled “Generating a typed client”Each scope publishes an OpenAPI 3.1 document, so you don’t have to hand-write request and response types. See OpenAPI Docs for the download links; the endpoints in this guide are all in the Shops scope.
API reference
Section titled “API reference”| Action | Method | Endpoint |
|---|---|---|
| List assortment | GET | /shops/{id}/products |
| Create packaging order | POST | /shops/{id}/orders |
| List orders | GET | /shops/{id}/orders |
| Get order | GET | /shops/{id}/orders/{order_id} |
| Create webhook | POST | /shops/{id}/webhooks |
| List webhooks | GET | /shops/{id}/webhooks |
Getting in touch
Section titled “Getting in touch”Integrating and stuck on something this guide doesn’t cover? Email helpdesk@re-zip.com, or ask us to set up a shared Slack channel.