Simsonic · Storefront API

Checkout API Reference

Request/response contract for the Shopify Customer Account & Hydrogen storefront checkout integration. This page documents fields that were previously undocumented but already live in production — keep this in sync with your integration.

POST /api/v1/checkout

POST https://<your-hyperbill-host>/api/v1/checkout

Prices the cart live, runs a real stock check, and creates a Stripe PaymentIntent. Authenticated with a shared StorefrontServiceToken — send it as Authorization: Bearer <token>. Content type application/json.

Request body

FieldTypeNotes
cart array required 1–20 line objects — see Cart line shape below.
customer_email string required The only field that gates checkout on its own. Used to resolve or create the buyer's Hyperbill account on payment success.
customer_country string required 2-letter ISO country code (e.g. GB). Used for VAT calculation.
customer_name string optional Max 200 chars. Used for account creation and email personalisation only.
customer_phone string optional Max 32 chars. Must include the country code (E.164 format) — e.g. +447700900123, not 07700900123. Without a phone, no activation/top-up SMS can be sent for that order (see sms_opt_in below, which is also required). Persists onto the buyer's Hyperbill account after their first order, so a later order/top-up that omits this field can still be reached if a number is already on file.
sms_opt_in boolean optional Must be the JSON boolean true exactly — any other value (omitted, false, or a truthy string like "true") is treated as no consent. Gates every activation/top-up SMS send: a phone number alone is not sufficient. The buyer's most recent explicit answer (on any order) is what's honoured going forward — an explicit false on a later order revokes a prior opt-in. Simply omitting the field is never treated as an opt-out of consent already on file.
customer_id string optional The buyer's Shopify Customer GID (gid://shopify/Customer/…). Never gates checkout on its own — supplements customer_email for durable identity matching if the buyer's Shopify email ever changes later.
Both customer_phone and sms_opt_in should be sent together, from whatever value is on file for the buyer at the moment checkout is created — not left blank if the checkout flow captures them later in the process. If either is missing when the order is created, no SMS goes out for that order, even if the buyer supplies a phone/consent afterward in the same session.

Cart line shape

FieldTypeNotes
webbing_sku string required Must match a published StorefrontPlan.webbing_sku.
quantity integer required 1–10. Must be exactly 1 for a top-up line.
topup_iccid string optional Marks this line as a top-up of an existing, already-owned device rather than a new purchase. Resolved against customer_email under either of two ownership shapes — see below. Any other ICCID (doesn't exist, or belongs to neither shape) returns the same 404 esim_not_found, never distinguishable.
Two ways topup_iccid can belong to customer_email: (1) customer_email resolves to the account that owns the device (a Consumer/Traveler's own device, or a Business Travel owner topping up anything in their fleet, assigned or not), or (2) the device is currently assigned to customer_email as a business team member (no separate Hyperbill account required for this path). An unassigned device only matches path (1) — a team member can never top up a device that isn't assigned to them. Once a top-up has been provisioned onto a device, later unassigning it does not retroactively invalidate that top-up.

Example request

{
  "cart": [
    { "webbing_sku": "GB-BIZ-10GB-1D", "quantity": 1 }
  ],
  "customer_email": "buyer@example.com",
  "customer_name": "Jane Buyer",
  "customer_country": "GB",
  "customer_phone": "+447700900123",
  "sms_opt_in": true,
  "customer_id": "gid://shopify/Customer/1234567890"
}

Response — 201 Created

{
  "order_id": "e8c60b08-41f5-4f87-8b5a-065b34c84c95",
  "client_secret": "pi_..._secret_...",
  "currency": "GBP",
  "subtotal": "6.49",
  "tax_amount": "1.30",
  "amount_total": "7.79"
}

Error responses

StatusCodeMeaning
400invalid_requestMalformed cart, missing required field, or bad country code.
400invalid_jsonRequest body isn't valid JSON.
400mixed_cart_not_allowedThe cart mixes Business (Business Travel/Business Top-up) and Travel (Travel/Travel Top-up) plans in one checkout — check out each category separately.
403business_purchase_not_allowedThe cart is Business-category, but customer_email already belongs to an existing Consumer/Traveler account — an existing account can never be upgraded to Business by a purchase.
404unknown_skuNo published plan matches webbing_sku.
404esim_not_foundA topup_iccid doesn't exist, or doesn't belong to customer_email.
409out_of_stockNo unassigned stock available for a non-top-up line.
422no_priceThe matched plan has no price configured.
422business_email_requiredThe cart is Business-category, but customer_email is on a known free/personal provider (Gmail, Hotmail, Yahoo, etc.) — Business plans require a business email domain.
502tax_failVAT calculation failed unexpectedly.
502stripe_errorStripe PaymentIntent creation failed.
503stripe_not_configuredStripe isn't configured on this environment.

POST /api/v1/physical-checkout

POST https://<your-hyperbill-host>/api/v1/physical-checkout

The physical Webbing SIM equivalent of /api/v1/checkout above — same auth (Authorization: Bearer <token>), same application/json, same response shape. A separate endpoint and a separate cart — a checkout here may only contain physical SIM SKUs, never mixed with eSIM SKUs from the other endpoint.

No instant delivery. A physical SIM has to be picked, assigned, and posted by a human — there is no QR code and nothing is provisioned automatically. On payment success, the buyer gets a receipt email and our team is notified to process the order. There is no topup_iccid concept on this endpoint.

Request body

FieldTypeNotes
cart array required 1–20 line objects: { "webbing_sku": string, "quantity": integer (1–10) }. webbing_sku must match a published physical SIM plan.
customer_email string required Same as /api/v1/checkout.
customer_country string required 2-letter ISO country code. Used for VAT calculation.
customer_name string optional Max 200 chars. Falls back to shipping_address.name if omitted, or vice versa — one of the two is required.
customer_phone string optional Max 32 chars. Not currently used for any SMS on this flow — kept for parity with /api/v1/checkout and future use.
customer_id string optional Same as /api/v1/checkout — the buyer's Shopify Customer GID.
shipping_address object required See Shipping address shape below.

Shipping address shape

FieldTypeNotes
namestringoptionalRecipient name. Falls back to customer_name if omitted — one of the two is required.
line1stringrequiredMax 200 chars.
line2stringoptionalMax 200 chars.
citystringrequiredMax 100 chars.
regionstringoptionalState/county/province, max 100 chars.
postal_codestringrequiredMax 20 chars.
countrystringrequired2-letter ISO country code.
phonestringoptionalDelivery contact number, may differ from customer_phone. Max 32 chars.

Example request

{
  "cart": [
    { "webbing_sku": "WPHYS-GLOBAL-5GB-30D", "quantity": 1 }
  ],
  "customer_email": "buyer@example.com",
  "customer_name": "Jane Buyer",
  "customer_country": "GB",
  "shipping_address": {
    "line1": "12 Example Street",
    "line2": "Flat 4",
    "city": "London",
    "region": "Greater London",
    "postal_code": "SW1A 1AA",
    "country": "GB",
    "phone": "+447700900123"
  }
}

Response — 201 Created

{
  "order_id": "3f9c1a2e-7b44-4d1a-9c2e-1a2b3c4d5e6f",
  "client_secret": "pi_..._secret_...",
  "currency": "GBP",
  "subtotal": "24.99",
  "tax_amount": "5.00",
  "amount_total": "29.99"
}

Error responses

StatusCodeMeaning
400invalid_requestMalformed cart, missing required field, or bad country code.
400invalid_jsonRequest body isn't valid JSON.
400invalid_shipping_addressshipping_address is missing, or missing/malformed name/line1/city/postal_code/country.
404unknown_skuNo published physical SIM plan matches webbing_sku.
409out_of_stockNot enough unassigned physical SIM stock for the requested quantity.
422no_priceThe matched plan has no price configured.
502tax_failVAT calculation failed unexpectedly.
502stripe_errorStripe PaymentIntent creation failed.
503stripe_not_configuredStripe isn't configured on this environment.

POST /api/v1/simpro-physical-checkout

POST https://<your-hyperbill-host>/api/v1/simpro-physical-checkout

The IoT Contract checkout endpoint — same auth (Authorization: Bearer <token>), same application/json, same response shape as /api/v1/checkout above. A separate endpoint and a separate cart — a checkout here may only contain IoT Contract SKUs, never mixed with eSIM or physical Webbing SKUs from the other two endpoints.

What this is for. Every buyer here is a business (Reseller account, created automatically on payment — there is no consumer path for this catalog). Price already covers the one-time SIM cost plus the first month; every month after that is billed separately outside Shopify, once our team manually completes setup. On payment success the buyer gets an email receipt, a text/SMS receipt if they opted in, and a separate emailed PDF invoice — no QR code, no instant provisioning, nothing else comes back from this endpoint itself.
Billing and delivery address are both required, and are genuinely separate. They can differ (e.g. head office vs a branch site the hardware ships to) — don't default one from the other beyond what shipping_address.name itself already falls back to.

Request body

FieldTypeNotes
cart array required 1–20 line objects: { "sku": string, "quantity": integer (1–10) }. sku must match a published IoT Contract plan.
customer_email string required Same as /api/v1/checkout.
customer_country string required 2-letter ISO country code. Used for VAT calculation.
customer_first_name string required Max 100 chars. Sent separately, not as one combined name field — this flows straight through to our CRM as real first/last name data.
customer_last_name string required Max 100 chars.
company_name string optional Max 200 chars. Falls back to the buyer's name if omitted. Used for the Reseller account name and our CRM's Company record.
company_registration_number string optional Max 64 chars.
vat_registration_number string optional Max 64 chars.
title string optional Max 200 chars. The buyer's own job title (not a salutation).
purchasing_contact object optional { "name": string, "email": string }, both optional within the object. May be a different person from the buyer.
invoice_contact object optional Same shape as purchasing_contact.
head_user object optional Same shape as purchasing_contact — the primary Simsonic user at the buyer's company.
customer_phone string optional Max 32 chars. Also gates the SMS receipt — only sent if sms_opt_in is also true.
sms_opt_in boolean optional Defaults to false (no SMS) if omitted — never inferred from a phone number being present alone.
customer_id string optional Same as /api/v1/checkout — the buyer's Shopify Customer GID.
marketing_consent object optional See Consent object shape below.
data_processing_consent object required See Consent object shape below. We cannot lawfully process this order's personal data without it — checkout fails with data_processing_consent_required if it's missing, not agreed, or incomplete.
billing_address object required See Billing address shape below.
shipping_address object required See Shipping/delivery address shape below. Genuinely separate from billing_address, not an alias of it.

Billing address shape

FieldTypeNotes
line1stringrequiredMax 200 chars.
line2stringoptionalMax 200 chars.
citystringrequiredMax 100 chars.
regionstringoptionalState/county/province, max 100 chars.
postal_codestringrequiredMax 20 chars.
countrystringrequired2-letter ISO country code.

Shipping/delivery address shape

FieldTypeNotes
namestringoptionalRecipient name. Falls back to customer_first_name + customer_last_name if omitted.
line1stringrequiredMax 200 chars.
line2stringoptionalMax 200 chars.
citystringrequiredMax 100 chars.
regionstringoptionalState/county/province, max 100 chars.
postal_codestringrequiredMax 20 chars.
countrystringrequired2-letter ISO country code.
phonestringoptionalDelivery contact number, may differ from customer_phone. Max 32 chars.

Consent object shape

These are compliance records, not preferences (unlike sms_opt_in) — we record exactly what the buyer was shown and when, since we don't own or version your consent copy ourselves. Send the real wording your form displayed and the real moment they agreed, not a placeholder.
FieldTypeNotes
agreedbooleanrequiredMust be true for data_processing_consent or checkout fails.
wordingstringrequiredThe exact consent copy shown to the buyer at that moment.
given_atstringrequiredISO-8601 timestamp of when the buyer agreed.

Example request

{
  "cart": [
    { "sku": "CONEXA-IOT-1", "quantity": 1 }
  ],
  "customer_email": "buyer@enterprise-example.com",
  "customer_first_name": "Jane",
  "customer_last_name": "Buyer",
  "customer_country": "GB",
  "company_name": "Enterprise Ltd",
  "company_registration_number": "12345678",
  "vat_registration_number": "GB123456789",
  "title": "Procurement Manager",
  "purchasing_contact": { "name": "Priya Purchasing", "email": "priya@enterprise-example.com" },
  "invoice_contact": { "name": "Ian Invoicing", "email": "ian@enterprise-example.com" },
  "head_user": { "name": "Hana Head", "email": "hana@enterprise-example.com" },
  "marketing_consent": {
    "agreed": true,
    "wording": "I'd like to receive marketing updates from Simsonic.",
    "given_at": "2026-08-26T09:00:00Z"
  },
  "data_processing_consent": {
    "agreed": true,
    "wording": "I agree to Simsonic processing our company's data for this order.",
    "given_at": "2026-08-26T09:00:00Z"
  },
  "billing_address": {
    "line1": "1 Head Office Street",
    "city": "London",
    "postal_code": "EC1A 1AA",
    "country": "GB"
  },
  "shipping_address": {
    "name": "Enterprise Ltd Branch",
    "line1": "99 Branch Depot Road",
    "city": "Manchester",
    "postal_code": "M1 1AA",
    "country": "GB",
    "phone": "+447700900123"
  }
}

Response — 201 Created

{
  "order_id": "3f9c1a2e-7b44-4d1a-9c2e-1a2b3c4d5e6f",
  "client_secret": "pi_..._secret_...",
  "currency": "GBP",
  "subtotal": "28.00",
  "tax_amount": "5.60",
  "amount_total": "33.60"
}

Error responses

StatusCodeMeaning
400invalid_requestMalformed cart, missing required field (including customer_first_name/customer_last_name), or bad country code.
400invalid_jsonRequest body isn't valid JSON.
400invalid_billing_addressbilling_address is missing, or missing/malformed line1/city/postal_code/country.
400invalid_shipping_addressshipping_address is missing, or missing/malformed name/line1/city/postal_code/country.
400data_processing_consent_requireddata_processing_consent is missing, agreed isn't true, or wording/given_at is missing.
404unknown_skuNo published IoT Contract plan matches sku.
422no_priceThe matched plan has no price configured.
502tax_failVAT calculation failed unexpectedly.
502stripe_errorStripe PaymentIntent creation failed.
503stripe_not_configuredStripe isn't configured on this environment.