API version: 2024-01 | ← Developer Program | Get API Access →

Overview

The IndeCommerce API lets you build apps, integrations, and automations for wholesale B2B merchants on the platform. It is intentionally Shopify-compatible — most code written against the Shopify Admin API works against our API with only a hostname change.

  • Admin REST API — full CRUD for products, orders, customers, inventory, collections, and more
  • Storefront GraphQL API — public-facing catalog browsing and cart/checkout mutations
  • Webhooks — real-time event delivery signed with HMAC-SHA256
  • App Billing API — charge merchants for your app via one-time charges, subscriptions, and usage-based billing
  • OAuth 2.0 — standard authorization-code flow; merchants install your app in one click
Shopify compatibility: IndeCommerce exposes the Shopify Admin API shim at /admin/api/<version>/. If your app already talks Shopify, point it at the merchant's IndeCommerce domain and it will work with no other changes.

Quickstart

1. Create a developer account

Go to /developers/signup and register. Your sandbox store is provisioned instantly.

2. Create an app

From your developer dashboard, click New App and fill in:

  • Name — displayed in the App Store listing
  • Slug — URL-safe identifier, e.g. my-inventory-sync
  • Redirect URIs — comma-separated OAuth callback URLs
  • Default scopes — space-separated list of scopes your app needs

3. Install on your sandbox

Use the install URL to trigger the OAuth flow against your sandbox store:

https://<your-domain>/<store-prefix>/apps/install?client_id=<client_id>&scope=read_products,write_orders&redirect_uri=https://yourapp.com/callback

4. Exchange the code for an access token

POST https://<your-domain>/api/v1/apps/oauth/token
Content-Type: application/x-www-form-urlencoded

client_id=<client_id>
&client_secret=<client_secret>
&code=<code_from_callback>
&redirect_uri=https://yourapp.com/callback

Response:

{
  "access_token": "ic_access_xxxxxxxxxxxx",
  "scope": "read_products write_orders",
  "token_type": "bearer"
}

5. Make your first API call

GET /admin/api/2024-01/products.json
X-Shopify-Access-Token: ic_access_xxxxxxxxxxxx

Sandbox Environment

Every developer account gets a dedicated sandbox store — a real IndeCommerce tenant pre-configured for testing. It is accessible from your developer dashboard under "Open sandbox store →".

  • No real payment processing — all Stripe interactions use Stripe's test mode
  • Isolated from production merchants; cannot appear in the public App Store
  • You can install your own apps on it without going through the review process
  • Use Stripe test card 4242 4242 4242 4242 for any payment flows
The sandbox prefix follows the pattern sandbox-dev{id}. All API calls scoped to your sandbox use this prefix.

OAuth 2.0

IndeCommerce uses the Authorization Code grant type. Each app install creates a unique access token scoped to one merchant store.

Authorization URL

GET /<store_prefix>/apps/install
  ?client_id=<your_client_id>
  &scope=read_products,write_orders
  &redirect_uri=https://yourapp.com/callback
  &state=<random_nonce>

Token exchange

POST /api/v1/apps/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&client_id=CLIENT_ID
&client_secret=CLIENT_SECRET
&code=CODE
&redirect_uri=REDIRECT_URI

Using the token

Pass the access token in every request using the X-Shopify-Access-Token header (Shopify-compat) or Authorization: Bearer:

X-Shopify-Access-Token: ic_access_xxxxxxxxxxxx

Token scopes

Tokens are valid indefinitely. A merchant can revoke an install from their app settings, which invalidates the token immediately.

API Keys

For server-to-server integrations that don't involve a merchant consent flow, merchants can generate API keys directly from their dashboard under Settings → API Keys. Keys carry the same permission model as OAuth tokens and are prefixed ic_key_.

curl https://<host>/admin/api/2024-01/products.json \
  -H "X-Shopify-Access-Token: ic_key_xxxxxxxxxxxx"

Scopes Reference

ScopeAccess
read_products Read products, variants, images, collections
write_products Create and update products, variants, images
read_orders Read orders and order line items
write_orders Create, update, and cancel orders
read_customers Read customer profiles and addresses
write_customers Create and update customers
read_inventory Read inventory levels and locations
write_inventory Adjust inventory levels
read_price_rules Read discount and price rules
write_price_rules Create and update price rules
read_analytics Read sales and traffic reports
read_metafields Read metafields on any resource
write_metafields Create and update metafields
read_webhooks List webhook subscriptions
write_webhooks Create and delete webhook subscriptions
read_storefront_tokens List storefront access tokens
write_storefront_tokens Create and delete storefront access tokens

Admin API — Basics & Versioning

The Admin API is available at two equivalent base paths:

  • Native: /api/v1/
  • Shopify shim: /admin/api/<version>/ — accepts any YYYY-MM version string; currently served from 2024-01

All requests must include the X-Shopify-Access-Token header. All responses are JSON. Pagination uses page_info cursors or limit/page params.

GET /admin/api/2024-01/products.json?limit=50&page_info=cursor123
X-Shopify-Access-Token: TOKEN

Products

GET/admin/api/2024-01/products.json — list products
GET/admin/api/2024-01/products/count.json — product count
GET/admin/api/2024-01/products/<id>.json — single product
POST/admin/api/2024-01/products.json — create product
PUT/admin/api/2024-01/products/<id>.json — update product
DELETE/admin/api/2024-01/products/<id>.json — delete product

Query parameters (list)

ParamTypeDescription
idsstringComma-separated list of product IDs
limitintegerMax results (default 50, max 250)
pageintegerPage number
titlestringFilter by title (partial match)
vendorstringFilter by vendor
product_typestringFilter by product type
statusstringactive | draft | archived
published_statusstringpublished | unpublished | any
fieldsstringComma-separated fields to return

Example: create a product

POST /admin/api/2024-01/products.json
{
  "product": {
    "title": "Ceramic Mug 12oz",
    "vendor": "Acme Ceramics",
    "product_type": "Mugs",
    "variants": [
      { "price": "18.00", "sku": "MUG-12-WHT", "inventory_quantity": 200 }
    ],
    "images": [{ "src": "https://cdn.example.com/mug.jpg" }]
  }
}

Variants

GET/admin/api/2024-01/products/<id>/variants.json
POST/admin/api/2024-01/products/<id>/variants.json
PUT/admin/api/2024-01/variants/<id>.json
DELETE/admin/api/2024-01/products/<product_id>/variants/<id>.json

Variants inherit product-level fields and add price, compare_at_price, sku, barcode, inventory_quantity, weight, option1/2/3.

Orders

GET/admin/api/2024-01/orders.json
GET/admin/api/2024-01/orders/count.json
GET/admin/api/2024-01/orders/<id>.json
POST/admin/api/2024-01/orders.json — create order
PUT/admin/api/2024-01/orders/<id>.json — update status / tracking
POST/admin/api/2024-01/orders/<id>/cancel.json — cancel order
POST/admin/api/2024-01/orders/<id>/close.json
GET/admin/api/2024-01/checkouts.json — abandoned checkouts

Order status values

pendingconfirmedprocessingshippeddeliveredcompleted. Cancellation moves to cancelled.

Customers

GET/admin/api/2024-01/customers.json
GET/admin/api/2024-01/customers/<id>.json
POST/admin/api/2024-01/customers.json
PUT/admin/api/2024-01/customers/<id>.json
DELETE/admin/api/2024-01/customers/<id>.json
GET/admin/api/2024-01/customers/<id>/addresses.json
POST/admin/api/2024-01/customers/<id>/addresses.json
PUT/admin/api/2024-01/customers/<id>/addresses/<addr_id>/default.json
DELETE/admin/api/2024-01/customers/<id>/addresses/<addr_id>.json

Inventory

GET/admin/api/2024-01/inventory_items.json
GET/admin/api/2024-01/inventory_items/<id>.json
GET/admin/api/2024-01/inventory_levels.json
POST/admin/api/2024-01/inventory_levels/adjust.json — adjust by delta
POST/admin/api/2024-01/inventory_levels/set.json — set absolute level
GET/admin/api/2024-01/locations.json

Example: adjust inventory

POST /admin/api/2024-01/inventory_levels/adjust.json
{
  "inventory_item_id": 808950810,
  "location_id": 487838322,
  "available_adjustment": -5
}

Collections

GET/admin/api/2024-01/custom_collections.json
GET/admin/api/2024-01/custom_collections/<id>.json
POST/admin/api/2024-01/custom_collections.json
PUT/admin/api/2024-01/custom_collections/<id>.json
DELETE/admin/api/2024-01/custom_collections/<id>.json
GET/admin/api/2024-01/collects.json — product-collection memberships
POST/admin/api/2024-01/collects.json — add product to collection
DELETE/admin/api/2024-01/collects/<id>.json

URL Redirects

GET/admin/api/2024-01/redirects.json
GET/admin/api/2024-01/redirects/count.json
GET/admin/api/2024-01/redirects/<id>.json
POST/admin/api/2024-01/redirects.json
PUT/admin/api/2024-01/redirects/<id>.json
DELETE/admin/api/2024-01/redirects/<id>.json

Storefront Access Tokens

Storefront tokens authenticate requests to the public Storefront GraphQL API.

GET/admin/api/2024-01/storefront_access_tokens.json
POST/admin/api/2024-01/storefront_access_tokens.json
DELETE/admin/api/2024-01/storefront_access_tokens/<id>.json

Metafields

Attach arbitrary key-value metadata to products, orders, or customers.

GET/admin/api/2024-01/metafields.json?metafield[owner_resource]=product&metafield[owner_id]=<id>
POST/admin/api/2024-01/metafields.json
PUT/admin/api/2024-01/metafields/<id>.json
DELETE/admin/api/2024-01/metafields/<id>.json

Example: set a metafield via GraphQL mutation

mutation {
  metafieldSet(metafields: [{
    ownerId: "gid://indecommerce/Product/123",
    namespace: "custom",
    key: "reorder_point",
    value: "50",
    type: "number_integer"
  }]) {
    metafields { id key value }
    userErrors { field message }
  }
}

Storefront GraphQL API

The Storefront API lets your app browse the merchant's public catalog, manage carts, and initiate checkout without requiring merchant-level credentials. Authenticate with a StorefrontAccessToken.

POST /storefront/api/graphql
X-Shopify-Storefront-Access-Token: <storefront_token>
Content-Type: application/json

{
  "query": "{ products(first: 10) { edges { node { id title } } } }"
}

Available queries

  • shop — store metadata
  • products(first, after, query) — paginated product list
  • product(id, handle) — single product with variants
  • collections(first) / collection(id, handle)
  • cart(id) — cart by ID
  • customer — authenticated customer (requires customer token)

Cart REST API

A lightweight JSON REST API for cart management — no authentication required for buyer-side calls. All endpoints are scoped to a merchant's store prefix (/<store_prefix>). Ideal for headless storefronts, custom themes, and apps like the Advanced Cart Drawer.

Session-based: Carts are tied to the buyer's session cookie (logged-in customers) or a cart_session_id session value (guests). No cart ID is needed — the server resolves the active cart automatically.

GET /<store_prefix>/api/cart

Return the current buyer's cart contents as JSON. Returns an empty cart object if no cart exists yet.

GET /mystore/api/cart
Cookie: session=...

// Response 200
{
  "success": true,
  "cart": {
    "item_count": 2,
    "subtotal": "49.98",
    "currency": "USD",
    "checkout_url": "/mystore/checkout",
    "cart_url": "/mystore/cart",
    "items": [
      {
        "id": 84,
        "product_id": 12,
        "variant_id": null,
        "product_name": "Widget Pro",
        "variant_name": null,
        "sku": "WGT-001",
        "quantity": 2,
        "price": "24.99",
        "line_total": "49.98",
        "image_url": "https://cdn.example.com/widget.jpg",
        "product_url": "/mystore/products/widget-pro"
      }
    ]
  }
}

POST /<store_prefix>/api/cart/add

Add an item to the cart. Accepts JSON or form data. If the same product+variant is already in the cart, the quantity is incremented.

POST /mystore/api/cart/add
Content-Type: application/json

{
  "product_id": 12,
  "variant_id": 31,   // optional — omit for products with no variants
  "quantity": 2
}

// Success 200
{ "success": true, "message": "Widget Pro added to cart!" }

// Error 400
{ "success": false, "message": "Only 5 items available in stock." }

Business rules enforced

  • Product must be active and belong to the store
  • External/affiliate products return 400 and cannot be added
  • Inventory check: quantity must not exceed inventory_quantity (unless allow_backorders is true)
  • Minimum order quantity per product and per buyer group are both enforced
  • If require_login_for_prices is on, guests receive 403

POST /<store_prefix>/api/cart/update

Update a cart item's quantity. Setting quantity to 0 removes the item.

POST /mystore/api/cart/update
Content-Type: application/json

{ "item_id": 84, "quantity": 3 }

// Success 200
{ "success": true, "item_count": 3 }

// Remove item (quantity 0)
{ "item_id": 84, "quantity": 0 }
// → { "success": true, "item_count": 0 }

POST /<store_prefix>/api/cart/remove

Remove an item from the cart entirely.

POST /mystore/api/cart/remove
Content-Type: application/json

{ "item_id": 84 }

// Success 200
{ "success": true, "item_count": 0 }

GET /<store_prefix>/api/cart/count

Lightweight endpoint for refreshing a cart badge. Returns the total item quantity across all line items.

GET /mystore/api/cart/count

{ "success": true, "count": 5 }

Error shape (all endpoints)

{ "success": false, "message": "Human-readable reason" }

Cart GraphQL API

The Storefront GraphQL API follows the Shopify Storefront API schema and is available at POST /storefront/api/graphql with a X-Shopify-Storefront-Access-Token header. It uses Global IDs (gid://indecommerce/Cart/1) for all resources.

The GraphQL cart uses a separate cart model (StorefrontCart) from the REST cart. It is designed for headless storefronts where you need to manage cart state client-side with a persistent cart ID. For theme-extension apps (like the Cart Drawer) the REST API is simpler and uses the server session.

cartCreate — create a new cart

mutation {
  cartCreate(input: {
    lines: [
      { merchandiseId: "gid://indecommerce/ProductVariant/31", quantity: 2 }
    ]
  }) {
    cart {
      id
      checkoutUrl
      cost { subtotalAmount { amount currencyCode } }
      lines(first: 10) {
        nodes {
          id quantity
          merchandise {
            ... on ProductVariant {
              id title price { amount }
              product { title featuredImage { url } }
            }
          }
        }
      }
    }
    userErrors { field message }
  }
}

cartLinesAdd — add items to an existing cart

mutation {
  cartLinesAdd(
    cartId: "gid://indecommerce/Cart/7"
    lines: [
      { merchandiseId: "gid://indecommerce/ProductVariant/31", quantity: 1 }
    ]
  ) {
    cart { id lines(first:10) { nodes { id quantity } } }
    userErrors { field message }
  }
}

cartLinesUpdate — change quantity of an existing line

mutation {
  cartLinesUpdate(
    cartId: "gid://indecommerce/Cart/7"
    lines: [
      { id: "gid://indecommerce/CartLine/84", quantity: 3 }
    ]
  ) {
    cart { id cost { subtotalAmount { amount currencyCode } } }
    userErrors { field message }
  }
}

cartLinesRemove — delete lines from the cart

mutation {
  cartLinesRemove(
    cartId: "gid://indecommerce/Cart/7"
    lineIds: ["gid://indecommerce/CartLine/84"]
  ) {
    cart { id lines(first:10) { nodes { id quantity } } }
    userErrors { field message }
  }
}

cart — query a cart by ID

query {
  cart(id: "gid://indecommerce/Cart/7") {
    id
    checkoutUrl
    createdAt updatedAt
    cost {
      subtotalAmount { amount currencyCode }
      totalAmount    { amount currencyCode }
    }
    buyerIdentity { email phone }
    lines(first: 50) {
      nodes {
        id quantity
        cost {
          amountPerQuantity { amount currencyCode }
          totalAmount       { amount currencyCode }
        }
        merchandise {
          ... on ProductVariant {
            id title sku
            price { amount currencyCode }
            image { url altText }
            product { id title handle featuredImage { url altText } }
          }
        }
      }
    }
  }
}

cartBuyerIdentityUpdate — attach customer identity

mutation {
  cartBuyerIdentityUpdate(
    cartId: "gid://indecommerce/Cart/7"
    buyerIdentity: { email: "[email protected]", phone: "+15550001234" }
  ) {
    cart { id buyerIdentity { email phone } }
    userErrors { field message }
  }
}

Cart type reference

FieldTypeDescription
id ID! Global ID — gid://indecommerce/Cart/{n}
checkoutUrl String! URL to the checkout page for this cart
lines CartLineConnection! Paginated list of cart line items
cost.subtotalAmount MoneyV2! Sum of all line totals before tax/shipping
cost.totalAmount MoneyV2! Same as subtotal (tax calculated at checkout)
buyerIdentity.email String Email attached to cart
buyerIdentity.phone String Phone attached to cart
createdAt DateTime! ISO 8601 creation timestamp
updatedAt DateTime ISO 8601 last-modified timestamp
note String Buyer note (visible to merchant)

Checkout GraphQL API

Convert a cart (or build directly) into a checkout, attach a shipping address and email, then complete it. All checkout mutations return a Checkout object and a checkoutUserErrors array.

checkoutCreate — create a checkout

mutation {
  checkoutCreate(input: {
    email: "[email protected]"
    lineItems: [
      { variantId: "gid://indecommerce/ProductVariant/31", quantity: 2 }
    ]
    shippingAddress: {
      firstName: "Jane", lastName: "Smith"
      address1: "123 Main St", city: "Austin"
      province: "TX", country: "US", zip: "78701"
    }
    note: "Leave at the dock"
  }) {
    checkout {
      id webUrl
      subtotalPrice { amount currencyCode }
      totalPrice    { amount currencyCode }
      lineItems(first: 10) { nodes { title quantity unitPrice { amount } } }
    }
    checkoutUserErrors { field message code }
  }
}

checkoutLineItemsAdd

mutation {
  checkoutLineItemsAdd(
    checkoutId: "gid://indecommerce/Checkout/abc123"
    lineItems: [{ variantId: "gid://indecommerce/ProductVariant/45", quantity: 1 }]
  ) {
    checkout { id lineItems(first:10) { nodes { id title quantity } } }
    checkoutUserErrors { field message }
  }
}

checkoutLineItemsUpdate

mutation {
  checkoutLineItemsUpdate(
    checkoutId: "gid://indecommerce/Checkout/abc123"
    lineItems: [{ id: "gid://indecommerce/CheckoutLineItem/77", quantity: 5 }]
  ) {
    checkout { id subtotalPrice { amount } }
    checkoutUserErrors { field message }
  }
}

checkoutLineItemsRemove

mutation {
  checkoutLineItemsRemove(
    checkoutId: "gid://indecommerce/Checkout/abc123"
    lineItemIds: ["gid://indecommerce/CheckoutLineItem/77"]
  ) {
    checkout { id lineItems(first:10) { nodes { id } } }
    checkoutUserErrors { field message }
  }
}

checkoutShippingAddressUpdateV2

mutation {
  checkoutShippingAddressUpdateV2(
    checkoutId: "gid://indecommerce/Checkout/abc123"
    shippingAddress: {
      firstName: "Jane", lastName: "Smith"
      address1: "456 Warehouse Blvd", city: "Austin"
      province: "TX", country: "US", zip: "78702"
    }
  ) {
    checkout { id shippingAddress { address1 city country } }
    checkoutUserErrors { field message }
  }
}

checkoutEmailUpdateV2

mutation {
  checkoutEmailUpdateV2(
    checkoutId: "gid://indecommerce/Checkout/abc123"
    email: "[email protected]"
  ) {
    checkout { id email }
    checkoutUserErrors { field message }
  }
}

checkoutCompleteFree — complete a zero-total checkout

mutation {
  checkoutCompleteFree(checkoutId: "gid://indecommerce/Checkout/abc123") {
    checkout { id completedAt order { id name } }
    checkoutUserErrors { field message }
  }
}
For paid checkouts, redirect the buyer to checkout.webUrl — the platform-hosted checkout page handles payment collection via Stripe. checkoutCompleteFree is only valid when the total is $0.00 (e.g. 100% discount codes or credit-covered orders).

Customer GraphQL API

Register buyers, authenticate them, and read their profile and order history using customer access tokens.

customerCreate — register a new buyer

mutation {
  customerCreate(input: {
    firstName: "Jane"
    lastName: "Smith"
    email: "[email protected]"
    password: "s3cur3P@ss!"
    phone: "+15550001234"
    acceptsMarketing: true
  }) {
    customer { id email firstName lastName }
    customerUserErrors { field message code }
  }
}

customerAccessTokenCreate — log in

mutation {
  customerAccessTokenCreate(input: {
    email: "[email protected]"
    password: "s3cur3P@ss!"
  }) {
    customerAccessToken {
      accessToken
      expiresAt
    }
    customerUserErrors { field message code }
  }
}

// Store accessToken in client; pass it as header for authenticated queries:
// X-Shopify-Storefront-Access-Token: <storefront_token>
// and include customerAccessToken in subsequent calls

customerUpdate — update profile

mutation {
  customerUpdate(
    customerAccessToken: "eyJ..."
    customer: {
      firstName: "Janet"
      phone: "+15550009999"
      acceptsMarketing: false
    }
  ) {
    customer { id firstName phone }
    customerUserErrors { field message }
  }
}

customerAccessTokenDelete — log out

mutation {
  customerAccessTokenDelete(customerAccessToken: "eyJ...") {
    deletedAccessToken
    deletedCustomerAccessTokenId
    userErrors { field message }
  }
}

customer query — fetch authenticated customer

query {
  customer(customerAccessToken: "eyJ...") {
    id email firstName lastName
    phone defaultAddress { address1 city country zip }
    orders(first: 5, sortKey: PROCESSED_AT, reverse: true) {
      nodes {
        id name
        processedAt
        financialStatus fulfillmentStatus
        currentTotalPrice { amount currencyCode }
        lineItems(first: 10) { nodes { title quantity } }
      }
    }
    addresses(first: 5) {
      nodes { id address1 city country zip isDefault }
    }
  }
}

Complete GraphQL operation reference

OperationKindDescription
shop query Store metadata, currency, domain
products query Paginated product catalog with variants
product query Single product by ID or handle
collections / collection query Collection list or single collection
cart query Fetch a cart by global ID
customer query Authenticated customer profile + orders
node / nodes query Fetch any resource(s) by global ID
cartCreate mutation Create a new cart with optional lines
cartLinesAdd mutation Append lines to an existing cart
cartLinesUpdate mutation Update line quantities/variants
cartLinesRemove mutation Remove lines from a cart
cartBuyerIdentityUpdate mutation Attach email/phone/customer to cart
checkoutCreate mutation Create a checkout with line items
checkoutLineItemsAdd mutation Append items to a checkout
checkoutLineItemsUpdate mutation Update checkout item quantities
checkoutLineItemsRemove mutation Remove items from a checkout
checkoutShippingAddressUpdateV2 mutation Set/update the shipping address
checkoutEmailUpdateV2 mutation Set/update the buyer email
checkoutCompleteFree mutation Complete a zero-total checkout
customerCreate mutation Register a new buyer account
customerUpdate mutation Update an authenticated buyer's profile
customerAccessTokenCreate mutation Authenticate buyer — returns token
customerAccessTokenDelete mutation Invalidate a buyer token (log out)

Webhook Subscriptions

Register an HTTPS endpoint to receive real-time event notifications when things happen in a merchant's store.

GET/admin/api/2024-01/webhooks.json
POST/admin/api/2024-01/webhooks.json
GET/admin/api/2024-01/webhooks/<id>.json
DELETE/admin/api/2024-01/webhooks/<id>.json

Example: subscribe to order creation

POST /admin/api/2024-01/webhooks.json
{
  "webhook": {
    "topic": "orders/create",
    "address": "https://yourapp.com/webhooks/order-created",
    "format": "json"
  }
}

Webhook Topics Reference

TopicFired when
orders/create A new order is placed
orders/updated An order is updated (status, tracking, etc.)
orders/paid An order transitions to paid status
orders/fulfilled An order is marked fulfilled / shipped
orders/cancelled An order is cancelled
products/create A new product is created
products/update A product or variant is updated
products/delete A product is deleted
customers/create A new customer registers
customers/update A customer profile is updated
customers/delete A customer is deleted
app/uninstalled A merchant uninstalls your app

Verifying Webhook Payloads

Every webhook request includes an X-Shopify-Hmac-Sha256 header containing a base64-encoded HMAC-SHA256 signature of the raw request body, signed with your app's client secret.

import hmac, hashlib, base64

def verify_webhook(body: bytes, hmac_header: str, secret: str) -> bool:
    digest = hmac.new(
        secret.encode('utf-8'),
        body,
        hashlib.sha256
    ).digest()
    computed = base64.b64encode(digest).decode('utf-8')
    return hmac.compare_digest(computed, hmac_header)
Always verify the signature before processing webhook payloads. Reject any request where the signature does not match.

Webhooks are delivered with a 10-second timeout. Failed deliveries are retried up to 19 times over 48 hours with exponential back-off. An endpoint that consistently fails will be automatically paused.

App Billing — Overview

Monetize your app through three charge types. All billing goes through IndeCommerce's Stripe integration — you receive 85% of each payment; the platform keeps 15%.

  • One-time charges — a single payment, e.g. a setup fee or add-on purchase
  • Recurring subscriptions — monthly or annual plan billing
  • Usage charges — metered charges posted against an active subscription (e.g. per-SMS sent)

All charges must go through a merchant confirmation page before they are activated. The flow:

  1. Your app creates a charge via the API → receives a confirmation_url
  2. Redirect the merchant to the confirmation_url
  3. Merchant accepts or declines on the IndeCommerce confirmation page
  4. Merchant is redirected to your return_url with ?charge_id=<id>
  5. Your app calls the activate endpoint to finalize the charge

One-time Charges

GET/admin/api/2024-01/application_charges.json
GET/admin/api/2024-01/application_charges/<id>.json
POST/admin/api/2024-01/application_charges.json
POST/admin/api/2024-01/application_charges/<id>/activate.json

Example: create a one-time charge

POST /admin/api/2024-01/application_charges.json
{
  "application_charge": {
    "name": "Premium Setup Fee",
    "price": "49.99",
    "return_url": "https://yourapp.com/billing/complete",
    "test": false
  }
}

// Response includes confirmation_url — redirect the merchant there
{
  "application_charge": {
    "id": 1017262353,
    "status": "pending",
    "confirmation_url": "https://<store>/apps/charges/confirm?token=..."
  }
}

Recurring Subscriptions

GET/admin/api/2024-01/recurring_application_charges.json
GET/admin/api/2024-01/recurring_application_charges/<id>.json
POST/admin/api/2024-01/recurring_application_charges.json
POST/admin/api/2024-01/recurring_application_charges/<id>/activate.json
DELETE/admin/api/2024-01/recurring_application_charges/<id>.json — cancel

Example: create a subscription

POST /admin/api/2024-01/recurring_application_charges.json
{
  "recurring_application_charge": {
    "name": "Professional Plan",
    "price": "29.99",
    "return_url": "https://yourapp.com/billing/activate",
    "trial_days": 14,
    "capped_amount": null
  }
}

Usage Charges

Post metered charges against an active recurring subscription. The subscription must have a capped_amount set.

GET/admin/api/2024-01/recurring_application_charges/<charge_id>/usage_charges.json
POST/admin/api/2024-01/recurring_application_charges/<charge_id>/usage_charges.json

Example: post a usage charge

POST /admin/api/2024-01/recurring_application_charges/455696195/usage_charges.json
{
  "usage_charge": {
    "description": "250 SMS messages sent",
    "price": "2.50"
  }
}

Rate Limits

The API uses a leaky-bucket algorithm. Each store-app pair has a bucket of 40 requests that refills at 2 requests/second.

HeaderDescription
X-Shopify-Shop-Api-Call-LimitCurrent usage, e.g. 32/40
Retry-AfterSeconds to wait when rate-limited (HTTP 429)

Errors

StatusMeaning
200 OK Request succeeded
201 Created Resource created
204 No Content Successful delete
400 Bad Request Malformed request or validation error — check errors[] in body
401 Unauthorized Missing or invalid access token
403 Forbidden Token lacks required scope
404 Not Found Resource not found or belongs to a different tenant
422 Unprocessable Entity Business logic validation failed
429 Too Many Requests Rate limit exceeded — respect Retry-After header
500 Internal Server Error Platform error — retry with back-off

Error bodies follow the Shopify shape:

{ "errors": { "title": ["can't be blank"] } }
// or for top-level errors:
{ "errors": "Not found" }

App Settings API

The App Settings API lets merchants configure your app without touching code. You declare a settings schema on your app listing — IndeCommerce automatically renders a form on the app detail page. Saved values are validated, persisted per-install, and injected into every Liquid theme extension render as the app_settings object.

How it works

  1. You declare a settings_schema JSON array on your AppListing (via your install script or the Developer Portal).
  2. IndeCommerce renders a schema-driven settings form on the merchant's app detail page.
  3. The merchant saves their values; they are stored per-install as a JSON object.
  4. At render time, defaults and merchant values are merged and injected as app_settings into every theme extension Liquid template.
  5. Your app can also read/write settings programmatically via the REST API.

Schema reference

settings_schema is a JSON array of field definition objects. Set it on your listing in your install script:

listing.settings_schema = json.dumps([
  {
    "key": "primary_color",   # used in Liquid: {{ app_settings.primary_color }}
    "type": "color",
    "label": "Accent Color",
    "default": "#1a1a1a",
    "description": "Checkout button and loading spinner color"
  },
  {
    "key": "drawer_width",
    "type": "number",
    "label": "Drawer Width (px)",
    "default": 420,
    "min": 280,
    "max": 600
  },
  {
    "key": "font_family",
    "type": "text",
    "label": "Font Family",
    "default": "",
    "max_length": 200,
    "description": "Leave blank to inherit the theme font"
  }
])

Supported field types

TypeMerchant inputValidation
colorColor picker + hex text boxMust match #RGB or #RRGGBB
textSingle-line text inputTruncated to max_length (default 500)
textareaMulti-line text areaTruncated to max_length
numberNumeric inputClamped to min/max if set
booleanCheckboxStored as true/false
selectDrop-down listMust be one of the declared options[].value strings

Field object properties:

PropertyRequiredDescription
keyYesSnake_case identifier. Used in values and in Liquid.
typeYesOne of the types listed above.
labelYesHuman-readable label shown in the form.
defaultNoValue returned when the merchant hasn't saved anything.
descriptionNoHelp text displayed below the field.
min / maxNoInclusive numeric bounds (number only).
optionsselect onlyArray of {"label": "...", "value": "..."} objects.
max_lengthNoMax character length for text/textarea (default 500).

Liquid access

Inside any theme extension Liquid template, schema defaults merged with merchant values are available as app_settings. Use Liquid's | default: filter for safety:

{% comment %} App Settings → CSS custom properties {% endcomment %}
<style>
:root {
  --accent:  {{ app_settings.primary_color | default: '#1a1a1a' }};
  --width:   {{ app_settings.drawer_width  | default: 420 }}px;
  --radius:  {{ app_settings.border_radius | default: 8 }}px;
  --font:    {{ app_settings.font_family   | default: "inherit" }};
}
</style>

<div style="max-width: var(--width);">
  <button style="background: var(--accent);">Checkout</button>
</div>

REST API

OAuth apps can read and write settings programmatically. Both endpoints are scoped to the authenticated install.

GET /admin/api/2024-01/settings.json

Returns the schema and merged values. Requires the read_settings scope.

GET /admin/api/2024-01/settings.json
X-IndeCommerce-Access-Token: ica_<token>

HTTP/1.1 200 OK
{
  "app_settings": {
    "schema": [ { "key": "primary_color", "type": "color", ... } ],
    "values": { "primary_color": "#4f46e5", "drawer_width": 380 }
  }
}

PUT /admin/api/2024-01/settings.json

Merges the supplied key/value pairs. Unknown keys are ignored. Requires the write_settings scope.

PUT /admin/api/2024-01/settings.json
X-IndeCommerce-Access-Token: ica_<token>
Content-Type: application/json

{ "settings": { "primary_color": "#4f46e5", "drawer_width": 380 } }

HTTP/1.1 200 OK
{ "app_settings": { "values": { "primary_color": "#4f46e5", "drawer_width": 380 } } }

Changelog

2024-01 (current)

  • Initial stable release of the Admin REST API and Storefront GraphQL API
  • OAuth 2.0 authorization code flow
  • Webhook subscriptions with HMAC-SHA256 signing
  • App Billing API: one-time charges, recurring subscriptions, usage charges
  • URL Redirects, Storefront Access Tokens, Metafields endpoints
  • Abandoned Checkouts API
  • Customer address management (create, update, delete, set default)
  • Storefront Cart REST APIGET /api/cart, POST /api/cart/add, /api/cart/update, /api/cart/remove, GET /api/cart/count
  • Storefront GraphQL expanded — full cart mutations (cartCreate, cartLinesAdd/Update/Remove, cartBuyerIdentityUpdate), checkout mutations, and customer mutations now documented with examples
  • App Settings API — declarative settings_schema on app listings, per-install merchant values, schema-driven settings form on app detail page, app_settings Liquid context variable in theme extensions, and GET/PUT /admin/api/2024-01/settings.json REST endpoints (read_settings / write_settings scopes)