ShopEazydevelopers
Get started

Quickstart: Storefront

Issue a key, list products, quote a basket and place an order.

You will issue a key in the console, read a store's products, quote a basket and place an order. It takes about ten minutes.

You need a store on the Growth or Scale plan, or on a live trial. A store on Starter cannot issue a key.

1. Issue a key

In the ShopEazy console:

  1. Open Settings, then Channels, then API.
  2. Make sure the API channel is on. While it is off, every request is refused with 403 api_channel_off.
  3. Under New key, type a label, for example Website (production), and choose Publishable.
  4. Select Create key, then copy the key.

The key is shown once. ShopEazy stores only a hash of it, so a lost key cannot be recovered: revoke it and create another.

A publishable key starts with sez_pub_ and is safe in a browser. A secret key starts with sez_sk_ and is for your server only. Read Storefront keys for what each can do.

Put the key in an environment variable. The examples below use a placeholder, sez_pub_....

export SHOPEAZY_KEY="sez_pub_..."

2. Read the store

Every request carries the key as a Bearer token. This one reads the store's public profile.

curl https://api.shopeazy.co/v1/store \
  -H "Authorization: Bearer $SHOPEAZY_KEY"

Responses come in an envelope, { "data": ... }. Here is a trimmed answer:

{
  "data": {
    "id": "store_123",
    "name": "Thandi's Teas",
    "subdomain": "thandis-teas",
    "customDomain": null,
    "currency": "ZAR",
    "membership": {
      "enabled": false,
      "gate": "checkout",
      "requireVerification": false,
      "applicationNote": null
    },
    "auth": { "tenantId": null },
    "payments": { "provider": "yoco", "name": "Yoco", "canCharge": true }
  }
}

payments.canCharge says whether a card checkout can be opened now. membership says whether the store sells to members only. Ids are opaque strings: use them as given.

With the typed client:

import { createStorefrontClient } from '@shopeazy/storefront-client';

const shopeazy = createStorefrontClient({ apiKey: process.env.SHOPEAZY_KEY! });

const store = await shopeazy.store.get();

See Libraries for how to install it and what its paths option does.

3. List products

curl "https://api.shopeazy.co/v1/store/products?limit=2" \
  -H "Authorization: Bearer $SHOPEAZY_KEY"

Lists answer { "data": { "items": [...], "nextCursor": ... } }. This is a trimmed answer for one product:

{
  "data": {
    "items": [
      {
        "id": "item_456",
        "kind": "physical",
        "title": "Rooibos tea, 100 bags",
        "slug": "rooibos-tea-100",
        "description": "Smooth, naturally sweet.",
        "price": { "amount": 11500, "currency": "ZAR" },
        "compareAtPrice": null,
        "taxable": true,
        "status": "active",
        "details": {
          "requiresShipping": true,
          "trackInventory": true,
          "stock": 24,
          "allowBackorder": false,
          "options": [],
          "variants": []
        }
      }
    ],
    "nextCursor": null
  }
}

Money is always { amount, currency } in minor units, so 11500 is R115,00. nextCursor is null on the last page. Otherwise send it back as cursor to get the next page. Only physical products are listed unless you pass kind, for example ?kind=service or ?kind=all.

With the client:

const { items: products, nextCursor } = await shopeazy.products.page({ limit: 2 });

4. Quote a basket

A quote prices a basket on the server. It creates nothing. Never trust a price your site worked out itself.

curl https://api.shopeazy.co/v1/store/checkout/quote \
  -H "Authorization: Bearer $SHOPEAZY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "lines": [{ "itemId": "item_456", "quantity": 2 }],
    "fulfilment": "collect"
  }'
{
  "data": {
    "fulfilment": "collect",
    "lines": [
      {
        "itemId": "item_456",
        "variantId": null,
        "title": "Rooibos tea, 100 bags",
        "variantTitle": null,
        "unitPrice": { "amount": 11500, "currency": "ZAR" },
        "quantity": 2
      }
    ],
    "totals": {
      "subtotal": { "amount": 23000, "currency": "ZAR" },
      "discount": { "amount": 0, "currency": "ZAR" },
      "shipping": { "amount": 0, "currency": "ZAR" },
      "tax": { "amount": 3000, "currency": "ZAR" },
      "total": { "amount": 23000, "currency": "ZAR" },
      "paid": { "amount": 0, "currency": "ZAR" },
      "balanceDue": { "amount": 23000, "currency": "ZAR" }
    },
    "delivery": null
  }
}

totals.tax is the VAT already inside the total, not added to it. A line for an item that is unknown or inactive is dropped without a word, so compare the quote's lines with your basket.

With the client:

const quote = await shopeazy.checkout.quote({
  lines: [{ itemId: 'item_456', quantity: 2 }],
  fulfilment: 'collect',
});

For delivery, send fulfilment: "delivery" with a shippingAddress. The quote then lists the delivery options. See Headless checkout.

5. Place the order

Send an Idempotency-Key so a retry after a dropped connection cannot place the order twice. Make one key per order attempt and reuse it on every retry.

curl https://api.shopeazy.co/v1/store/orders \
  -H "Authorization: Bearer $SHOPEAZY_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5f0c1d7e-3a52-4c0b-9a1e-7d9c2b6e41aa" \
  -d '{
    "lines": [{ "itemId": "item_456", "quantity": 2 }],
    "fulfilment": "collect",
    "customer": {
      "name": "Sipho Dlamini",
      "email": "sipho@example.co.za",
      "phone": "+27821234567"
    }
  }'

Only customer.name is required. The answer is 201 with the order and an accessToken:

{
  "data": {
    "id": "order_789",
    "number": "1042",
    "status": "open",
    "paymentStatus": "unpaid",
    "fulfillmentStatus": "unfulfilled",
    "currency": "ZAR",
    "totals": {
      "subtotal": { "amount": 23000, "currency": "ZAR" },
      "discount": { "amount": 0, "currency": "ZAR" },
      "shipping": { "amount": 0, "currency": "ZAR" },
      "tax": { "amount": 3000, "currency": "ZAR" },
      "total": { "amount": 23000, "currency": "ZAR" },
      "paid": { "amount": 0, "currency": "ZAR" },
      "balanceDue": { "amount": 23000, "currency": "ZAR" }
    },
    "customer": { "name": "Sipho Dlamini", "email": "sipho@example.co.za", "phone": "+27821234567" },
    "lines": [
      {
        "itemId": "item_456",
        "title": "Rooibos tea, 100 bags",
        "variantTitle": null,
        "quantity": 2,
        "unitPrice": { "amount": 11500, "currency": "ZAR" },
        "fulfillmentStatus": "unfulfilled",
        "booking": null
      }
    ],
    "createdAt": 1792636800000,
    "paidAt": null,
    "accessToken": "<order access token>"
  }
}

Keep accessToken. It is returned only here, it is valid for 30 days, and it is the only way a guest reads this order back. Treat it like a password. Timestamps are epoch milliseconds.

With the client, which sends a fresh Idempotency-Key for you by default:

const key = crypto.randomUUID(); // one per order attempt, reused on retries

const order = await shopeazy.orders.create(
  {
    lines: [{ itemId: 'item_456', quantity: 2 }],
    fulfilment: 'collect',
    customer: { name: 'Sipho Dlamini', email: 'sipho@example.co.za' },
  },
  { idempotencyKey: key },
);

// order.accessToken: keep it for the confirmation page

6. Read the order back

The order id alone reads nothing. Send the accessToken as X-Order-Token.

curl https://api.shopeazy.co/v1/store/orders/order_789 \
  -H "Authorization: Bearer $SHOPEAZY_KEY" \
  -H "X-Order-Token: <order access token>"
const read = await shopeazy.orders.get(order.id, { orderToken: order.accessToken });

Next

The order is unpaid. To take payment, start a checkout and send the shopper to it, then confirm on your success page. That is the rest of Headless checkout.

Handle failures

Every error has a code. Branch on the code, never on the message. See Errors.

On this page