NAV

The Basics

Booqable apps are modular extensions that add new functionality to a Booqable account or its online store. They're built by third-party developers and distributed through the App Store, where merchants discover, install, and manage them.

An app is fundamentally one file — a booqable.json manifest — plus whatever templates, assets, and backend code it needs. Depending on what your app does, it can:

An app can combine more than one of these — the example payment app on GitHub shows a complete OAuth + payment-options + embedded-page integration if you want a working reference to read alongside these docs.

App Manifest

Every app ships a booqable.json manifest at its root — the one file Booqable requires. Alongside it, an app includes whatever templates, assets, and locale files it needs:

app-name/
├── booqable.json          # required, at the app's root
├── assets/                # convention
├── config/
│   └── locales/           # required: *.yaml, one per locale
└── templates/             # convention, only if you declare
                            # tracking_script or theme_blocks

Files are referenced from booqable.json by their exact relative path — organize them however makes sense for your app. Folders like assets or templates are common conventions, not requirements. The one structural rule besides booqable.json itself: locale files live under config/locales/ as .yaml files (e.g. en.yaml, pt_BR.yaml). The top-level key in each file (en:) defines its locale, and every localization key you use must exist in every locale file.

booqable.json is validated against a versioned JSON Schema, declared by its own $schema field — see Reference for the exact current shape.

Settings UIs, plan pricing, and locale file content are covered in depth in Configuration.

App Store Listing

Your app's store listing — the card in the App Store, its detail page, and its plan picker — is built entirely from fields in booqable.json's app_store object and plans array:

The plans field itself is optional, defaulting to an empty array for a free app, for example, but if given then each plan under it has its own listing fields:

Capabilities

Each section below covers one thing your app can do inside Booqable: Tracking Scripts, Theme Blocks, Delivery Carriers, Payment Options, and Embedded Pages. User Framework sits alongside them, but it isn't something you pick on its own. It's the JavaScript API shared by Tracking Scripts and Theme Blocks.

Read only the sections relevant to what you're building. A real app commonly combines more than one, for example a payment app that also embeds a settings page.

Tracking Scripts

Tracking scripts let your app inject JavaScript into every storefront page — useful for analytics, marketing pixels, and similar integrations.

// booqable.json
{
  "tracking_script": {
    "template": { "asset": "templates/index.js.liquid" }
  }
}

Only JavaScript is allowed: the referenced template must be a Liquid file that renders to valid JavaScript. It is inlined into the page inside a load handler and is not validated, so a syntax error will break your script. Declarations at the top level are not global, so attach anything other code needs to window.

Merchant-entered values (like an API key) come from a configuration_page form field, not a manifest key — see Configuration. Whatever the merchant enters is available in your template as a variable named after the field, e.g. const apiKey = '{{ api_key }}'.

Your script only renders once the app is installed and configured, so if you have required form fields, nothing is injected until the merchant has saved them.

Inside your template, use the User Framework to hook into storefront events (viewProduct, addToCart, checkout steps, ...) and to register for cookie consent.

If your script sets cookies, register it with Booqable.registerApp so Booqable can gate it behind visitor consent. One real caveat: this only actually gates anything if the merchant has enabled the cookie notice in their theme. If they haven't, onConsent fires automatically for every visitor with no interaction at all — design your integration to tolerate either case.

Example

// templates/index.js.liquid
Booqable.registerApp('marketing', {
  name: "Google Analytics",
  domain: "googletagmanager.com",

  init: function () {
    window.dataLayer = window.dataLayer || []
    window.gtag = function () { window.dataLayer.push(arguments) }
    window.gtag('consent', 'default', { ad_storage: 'denied', analytics_storage: 'denied' })
    window.gtag('js', new Date())
    window.gtag('config', '{{ api_key }}')
    Booqable.loadScript('https://www.googletagmanager.com/gtag/js?id={{ api_key }}')
  },

  onConsent: function () {
    window.gtag('consent', 'update', { ad_storage: 'granted', analytics_storage: 'granted' })
  },

  onDeny: function () {
    window.gtag('consent', 'update', { ad_storage: 'denied', analytics_storage: 'denied' })
  }
})

This example does its setup in init (always called on page load) and relies on GA4's own consent mode (gtag('consent', ...)) rather than gating inside onConsent/onDeny directly — a valid alternative when the third-party library you're wrapping has its own consent-aware mode.

Theme Blocks

Theme blocks let your app add custom content (widgets, forms, embeds) that merchants place into their website via the theme editor.

// booqable.json
{
  "theme_blocks": [
    {
      "name": "Newsletter Signup",
      "template": { "asset": "templates/newsletter-signup.liquid" },
      "allowed_sections": ["app"],
      "settings": {
        "title": { "type": "text", "label": "Title", "default": "Subscribe to our newsletter" }
      }
    }
  ]
}

Settings types

Each setting's value is available in your template as a Liquid variable named after the setting key (e.g. a height setting renders as {{ height }}).

Merchant-entered global values (like an API key shared across every block instance) come from a configuration_page form field, not a manifest key — see Configuration. Whatever the merchant enters is available in your template as a variable named after the field, e.g. const apiKey = '{{ api_key }}'.

Your block only renders once the app is installed and configured, so if you have required form fields, it won't appear until the merchant has saved them.

If your block sets cookies, register it with Booqable.registerApp (see User Framework) so Booqable can gate it behind visitor consent. Same real caveat as tracking scripts: this only gates anything if the merchant has enabled the cookie notice in their theme. If they haven't, onConsent fires automatically with no visitor interaction. Design your integration to tolerate either case.

Each block instance gets a unique app_id variable during rendering, useful for scoping CSS or JS to that specific instance:

<div id="{{ app_id }}">
  <style>#{{ app_id }} .widget { /* ... */ }</style>
</div>

User Framework

The User Framework is a client-side JavaScript API, available as window.Booqable inside your Tracking Scripts and Theme Blocks templates. It doesn't apply to any other capability.

Registering your app

Booqable.registerApp('marketing', {
  name: "My App",
  domain: "example.com",

  init: function () {
    // Always runs on page load, regardless of consent.
  },

  onConsent: function () {
    // See "Consent gating" below: not guaranteed to run.
  },

  onDeny: function () {
    // See "Consent gating" below: not guaranteed to run.
  }
})

Your template's top-level code runs with or without registerApp. In a theme block it runs as the page is parsed. In a tracking script it runs on the window load event, since the script is wrapped in a load handler. What registerApp actually gets you is a structured init callback that fires on the window load event, plus the onConsent/onDeny consent hooks below. If your script doesn't need to react to consent, you don't need registerApp at all. Call registerApp synchronously at the top level of your template. Registering later, for example inside a timeout or a callback, won't get init.

The first argument is a category: marketing or essential. In practice almost every real app registers as marketing. The two categories aren't symmetric (see below), and essential is rarely used.

onConsent/onDeny are meant to gate cookie-setting behind the visitor's choice, but whether they fire at all depends on a setting the merchant controls, not something your app can see or influence:

Practical implication: don't assume onConsent/onDeny reflect real, deliberate visitor consent. A merchant without the cookie notice enabled effectively grants blanket consent to every app on every page load. If your integration has real compliance requirements, treat this as a merchant configuration issue to flag, not something your app code can enforce on its own.

Landing params

When a visitor lands on the store with query params, Booqable saves them and adds them to cart and checkout links. That way a script that only loads after consent, or one running on the checkout's domain, can still read a click ID from the current URL.

Declare the params your script reads in registerApp:

Booqable.registerApp('marketing', {
  name: "My App",
  tracking: {
    landingParams: [
      { clickIdParam: 'myref', extraParams: ['mysub'] } // ?myref=123&mysub=newsletter
    ]
  }
})

Links to another domain (such as the checkout of an embedded store) get the saved params for up to 30 days. Links on the same site only get them during the visit they arrived in, except well-known ad click IDs such as gclid, which are kept there for 30 days too.

Booqable.captureLandingParams

Once your script has stored the click ID itself, for example in a cookie, call this so Booqable stops adding your declared params to links on the same site:

Booqable.captureLandingParams("My App") // the name you passed to registerApp

Links to another domain are not affected.

Events

Booqable.on('addToCart', function (data) {
  // data: { value, currency, items: [{ item_id, item_name, quantity, price }] }
})

Booqable.on('completed', function () {
  // no argument, read Booqable.cartData instead
})

Available events: viewProduct, addToCart, removeFromCart, information, payment, completed, page-change.

Only viewProduct, addToCart, and removeFromCart receive a data argument directly. Every other event fires with no argument at all, so read Booqable.cartData instead.

page-change fires once per checkout page load (information, payment, completed). It does not fire on ordinary storefront navigation (home, product, listing, or static pages).

Booqable.cartData

Only available in the checkout events (information, payment, completed). Your app should only read it, never assign to it. It includes:

cartId, orderId, orderNumber, currency, coupon, couponDiscount, deposit, toBePaid, totalDueLater, grandTotal, grandTotalWithTax, tax, deliveryPrice, items, email, name.

Utilities

window.Booqable also exposes additional methods used internally by Booqable's own first-party integrations. These aren't part of this API and may change without notice, so stick to what's documented here.

Example

Booqable.registerApp('marketing', {
  name: "Example Tracker",
  domain: "example.com",

  onConsent: function () {
    Booqable.on('page-change', function () { /* ... */ })
    Booqable.on('viewProduct', function (data) { /* ... */ })
    Booqable.on('completed', function () {
      var total = Booqable.cartData.grandTotalWithTax
      // ...
    })
  }
})

This pattern, subscribing to events only inside onConsent, is the safe default when the library you're integrating has no consent-aware mode of its own. If it does (as GA4's gtag('consent', ...) does), setting up in init and relying on the library's own gating is a valid alternative. See the Tracking Scripts example.

Delivery Carriers

Delivery carriers let your app offer custom shipping rates at checkout. Unlike tracking scripts or theme blocks, this isn't declared in the manifest at all. You register your carrier through the API once your app is installed.

Registering a carrier

// POST /api/4/app_carriers (request)
{
  "data": {
    "type": "app_carriers",
    "attributes": {
      "identifier": "my-carrier",
      "rates_url": "https://example.com/api/rates"
    }
  }
}

POST /api/4/app_carriers, typically called right after your OAuth install completes.

A carrier can also be updated later with PATCH/PUT /api/4/app_carriers/{id}, same attributes.

Fetching rates

// your rates_url responds with this (response)
{
  "data": [
    {
      "id": "standard-shipping",
      "type": "delivery_rates",
      "attributes": {
        "identifier": "standard-shipping",
        "label": "Standard shipping",
        "carrier_id": "7c1f0a52-3b9e-4d6a-8e24-5f2a9c3b1d70", // id returned when you registered the carrier
        "price_in_cents": 500,
        "minimum_order_amount_in_cents": 0
      }
    }
  ]
}

When a customer requests delivery rates at checkout, Booqable sends a POST request to your rates_url with a form-encoded body (application/x-www-form-urlencoded) containing:

Coordinates are sent as a "lat,lng" string and addresses as single-line strings. token is a JWT signed with your app's OAuth client secret; verify it to confirm the request came from Booqable.

Booqable waits 3 seconds for a response before treating the request as failed.

Booqable sends this request to every active carrier on every rates request. If you don't serve a request, for example because the destination is outside your coverage, respond with an empty data array. If your carrier fails or times out, Booqable skips it and uses the rates from the other carriers.

Each rate's own identifier is yours to choose (e.g. standard-shipping, express-shipping) and is separate from the carrier's identifier used at registration. label is shown to the customer; if omitted, the rate's identifier is used.

Each rate must have an id, a type of delivery_rates, and attributes including carrier_id (the id Booqable returned when you registered the carrier) and price_in_cents. If any rate in the response is invalid, Booqable discards the entire response for your carrier.

Uninstalling

Uninstalling the app archives all of its carriers, removes their location links, and re-enables pickup at any location left without a carrier.

Payment Options

This capability allows your app to add custom payment options for users during checkout.

Routes

// booqable.json
{
  "base_url": "https://my-payments-app.com",
  "payments": {
    "routes": {
      "charge_url": { "relative": "/mypay/charge" },
      "cancel_charge_url": { "relative": "/mypay/charge/:id/cancel" },
      "authorization_url": { "relative": "/mypay/auth" },
      "capture_authorization_url": { "relative": "/mypay/auth/:id/capture" },
      "void_authorization_url": { "relative": "/mypay/auth/:id/void" },
      "refund_url": { "relative": "/mypay/refund" }
    },
    "pay_button": { "asset": "assets/pay-button.svg" }
  }
}

Payment apps define their API routes under the payments.routes object of their booqable.json file. These routes tell Booqable where to send requests for different payment operations. Routes are relative to your manifest's base_url, or absolute URLs. pay_button is a separate, optional asset reference rendered as the clickable payment button in checkout.

There are a total of 6 routes available:

Configuration

// POST /api/4/app_payment_options (request)
{
  "data": {
    "type": "app_payment_options",
    "attributes": {}
  }
}

After a user installs your payment app, you must create a PaymentOption record using Booqable's Payment Options API endpoint. The PaymentOption enables Booqable to show new payment options in the UI using the routes configured in your app's booqable.json file. Users can then select your payment method during checkout, and Booqable will make authenticated requests to your app's endpoints using JWT tokens for security.

An empty attributes object is enough: name, identifier, and all 6 routes are copied from your manifest automatically.

Create the PaymentOption right after your app exchanges its OAuth code for an access token (see OAuth), or once the user has provided any other settings your app needs.

Tip: If you're using the Booqable App Rails engine, you can schedule the PaymentOption creation in a background job from the AppInstalledJob hook.

There's no update endpoint for a payment option once created. To change a route later, archive and recreate it.

Available Routes

charge_url
// Example:
// POST https://my-payments-app.com/charge
┌──────────────┐                    ┌──────────────┐                    ┌─────────────┐
│   Booqable   │                    │  Payment App │                    │  Customer   │
└──────┬───────┘                    └──────┬───────┘                    └──────┬──────┘
       │                                   │                                   │
       │  1. POST to charge_url            │                                   │
       │──────────────────────────────────>│                                   │
       │                                   │                                   │
       │  2. Return { id, redirect_url }   │                                   │
       │<──────────────────────────────────│                                   │
       │                                   │                                   │
       │  3. Booqable redirects to `redirect_url`                              │
       │──────────────────────────────────>│                                   │
       │                                   │                                   │
       │                                   │  4. Customer completes payment    │
       │                                   │<──────────────────────────────────│
       │                                   │                                   │
       │  5. PUT /api/4/payment_charges    │                                   │
       │<──────────────────────────────────│                                   │
       │         (update status)           │                                   │
       │                                   │                                   │
       │  6. Redirect to `return_url`      │                                   │
       │<──────────────────────────────────│                                   │
       │                                   │                                   │
       │  7. Customer sees success page    │                                   │
       │<──────────────────────────────────────────────────────────────────────│

Process payment charges.

This route is called when a user initiates a payment during checkout. Booqable sends the payment details including the total amount, deposit amount, customer address, and a return URL where the user should be redirected after payment completion. Your app should respond with a unique charge ID and a redirect URL where the user can complete the payment.

Flow:

  1. Booqable sends a POST request to your charge_url with the payment details.
  2. Respond with your internal charge id and a redirect_url for the customer.
  3. Booqable redirects the customer to your redirect_url to complete payment.
  4. Once the customer completes payment, update the PaymentCharge status (succeeded, failed).
  5. Redirect the customer back to the return_url provided in step 1.
Request Parameters
// POST charge_url (request)
{
  "booqable_id": "a3f1c9e2-4b7d-4e8a-9c2b-1f5e6d7a8b9c",
  "customer_address": "123 Main St, City, State, Country",
  "total_in_cents": 5000,
  "deposit_in_cents": 1000,
  "currency": "USD",
  "return_url": "https://booqable.com/example-end-of-payment-callback"
}
Parameter Description
booqable_id The unique identifier of this charge in Booqable's system
customer_address The customer's default address as a single line, or null if there isn't one
total_in_cents Total amount to charge in cents (e.g., 5000 = $50.00)
deposit_in_cents The portion of total_in_cents that is a deposit, in cents
currency Currency code for the payment (e.g., "USD", "EUR")
return_url URL where the user should be redirected after payment completion
Response Parameters
// your charge_url responds with this (response)
{
  "id": "charge_123",
  "redirect_url": "https://my-payments-app.com/redirect"
}
Parameter Description
id Unique identifier for the charge, in your payment app's system
redirect_url URL where the user should be redirected to, from Booqable, to complete the payment
cancel_charge_url
// Example:
// PUT https://my-payments-app.com/charge/charge_123/cancel

Cancel a payment charge.

This route is called when a payment needs to be cancelled, typically when an order is cancelled or when a shopping cart is invalidated. The charge ID from your app is passed in the URL path, and you should respond with a success status to confirm the cancellation.

Flow:

  1. Booqable sends a PUT request to your cancel_charge_url with your charge ID in the URL path and an empty JSON body ({}).
  2. Cancel the pending charge on your payment provider (if applicable).
  3. Respond with HTTP 200 OK to confirm cancellation.

Note: No API callback is required. Booqable handles the status update internally after receiving your success response. Errors are logged but don't block cancellation on Booqable's side.

Request Parameters
Parameter Description
:id (URL path) The charge ID, in your payment app's system, to cancel
Response

The response should be an HTTP 200 OK status.

authorization_url
// Example:
// POST https://my-payments-app.com/authorization
┌──────────────┐                    ┌──────────────┐                    ┌─────────────┐
│   Booqable   │                    │  Payment App │                    │  Customer   │
└──────┬───────┘                    └──────┬───────┘                    └──────┬──────┘
       │                                   │                                   │
       │  1. POST to authorization_url     │                                   │
       │──────────────────────────────────>│                                   │
       │                                   │                                   │
       │  2. Return { id, redirect_url }   │                                   │
       │<──────────────────────────────────│                                   │
       │                                   │                                   │
       │  3. Booqable redirects to `redirect_url`                              │
       │──────────────────────────────────>│                                   │
       │                                   │                                   │
       │                                   │  4. Customer authorizes hold      │
       │                                   │<──────────────────────────────────│
       │                                   │                                   │
       │  5. PUT /api/4/payment_authorizations                                 │
       │<──────────────────────────────────│                                   │
       │          (update status)          │                                   │
       │                                   │                                   │
       │  6. Redirect to `return_url`      │                                   │
       │<──────────────────────────────────│                                   │

Create payment authorizations for deposit/capture flows.

This route is called when creating a payment authorization for later capture, typically used for deposit and hold scenarios where funds are authorized but not immediately charged. The response should include an authorization ID and redirect URL for the user to complete the authorization process.

Flow:

  1. Booqable sends a POST request to your authorization_url with the authorization details.
  2. Respond with your internal authorization id and a redirect_url for the customer.
  3. Booqable redirects the customer to your redirect_url to complete the authorization.
  4. Once the customer authorizes the hold, update the PaymentAuthorization status (succeeded, failed).
  5. Redirect the customer back to the return_url provided in step 1.
Request Parameters
// POST authorization_url (request)
{
  "booqable_id": "a3f1c9e2-4b7d-4e8a-9c2b-1f5e6d7a8b9c",
  "customer_address": "123 Main St, City, State",
  "total_in_cents": 5000,
  "currency": "USD",
  "return_url": "https://booqable.com/example-end-of-authorization-callback"
}

Unlike charge_url, this request has no deposit_in_cents field.

Parameter Description
booqable_id The unique identifier of this authorization in Booqable's system
customer_address The customer's default address as a single line, or null if there isn't one
total_in_cents Total amount to authorize in cents (e.g., 5000 = $50.00)
currency Currency code for the authorization (e.g., "USD", "EUR")
return_url URL where the user should be redirected after authorization completion
Response Parameters
// your authorization_url responds with this (response)
{
  "id": "authorization_123",
  "redirect_url": "https://my-payments-app.com/redirect"
}
Parameter Description
id Unique identifier for the authorization in your payment app's system
redirect_url URL where the user should be redirected to, from Booqable, to complete the authorization
capture_authorization_url
// Example:
// PUT https://my-payments-app.com/authorization/authorization_123/capture
┌──────────────┐                    ┌──────────────┐
│   Booqable   │                    │  Payment App │
└──────┬───────┘                    └──────┬───────┘
       │                                   │
       │  1. PUT to capture_authorization_url
       │──────────────────────────────────>│
       │                                   │
       │  2. Return 200 OK                 │
       │<──────────────────────────────────│
       │                                   │
       │                                   │  3. Capture funds on provider
       │                                   │
       │  4. PUT /api/4/payment_charges    │
       │<──────────────────────────────────│
       │          (update status)          │

Capture funds from a previous authorization.

This route is called when capturing previously authorized funds, completing the deposit and hold flow. The authorization ID from your app is passed in the URL path, and the request body contains the final amount to capture along with the Booqable charge ID. Your app should respond immediately with HTTP 200 OK. After the actual capture of the funds is complete you must update the PaymentCharge status via the Booqable API.

Flow:

  1. Booqable sends a PUT request to your capture_authorization_url with the authorization ID in the URL path.
  2. Respond immediately with HTTP 200 OK (the charge is saved as started in Booqable).
  3. Capture the funds on your payment provider.
  4. Update the PaymentCharge status (succeeded, failed).
Request Parameters
// PUT capture_authorization_url/:id (request)
{
  "total_in_cents": 5000,
  "booqable_id": "a3f1c9e2-4b7d-4e8a-9c2b-1f5e6d7a8b9c"
}
Parameter Description
total_in_cents Final amount to capture in cents (may be less than or equal to the authorized amount)
booqable_id The unique identifier of the new PaymentCharge representing the captured funds

Note: Authorizations can only be captured once. If you capture less than the authorized amount, the remaining funds are released back to the customer.

Response

The response should be an HTTP 200 OK status. The body is ignored. If you respond with an error status, Booqable discards the new PaymentCharge and the capture fails. Include a JSON error message to explain why.

void_authorization_url
// Example:
// PUT https://my-payments-app.com/authorization/authorization_123/void

Void/cancel a payment authorization.

This route is called when cancelling an unused authorization, typically when an order is cancelled before the authorization is captured or when the authorization expires. The authorization ID from your app is passed in the URL path, and you should respond with a success status to confirm the void operation.

Flow:

  1. Booqable sends a PUT request to your void_authorization_url with the authorization ID in the URL path and an empty JSON body ({}).
  2. Release the held funds on your payment provider.
  3. Respond with HTTP 200 OK to confirm the void.

Note: No API callback is required. Booqable handles the status update internally after receiving your success response. A 404 response is treated as an already-voided authorization, not an error.

Request Parameters
Parameter Description
:id (URL path) The authorization ID, in your payment app's system, to void
Response

The response should be an HTTP 200 OK status.

refund_url
// Example:
// POST https://my-payments-app.com/refund
┌──────────────┐                    ┌──────────────┐
│   Booqable   │                    │  Payment App │
└──────┬───────┘                    └──────┬───────┘
       │                                   │
       │  1. POST to refund_url            │
       │──────────────────────────────────>│
       │                                   │
       │  2. Return { id }                 │
       │<──────────────────────────────────│
       │                                   │
       │                                   │  3. Process refund on provider
       │                                   │
       │  4. PUT to /api/4/payment_refunds │
       │<──────────────────────────────────│
       │          (update status)          │

Process payment refunds.

This route is called when processing a refund for a completed payment. Booqable sends the refund details including the original charge ID, refund amount, deposit amount, and currency. Your app should respond with a unique refund ID to track the refund operation.

Flow:

  1. Booqable sends a POST request to your refund_url with the refund details.
  2. Respond immediately with your internal refund id (the refund is saved as pending in Booqable).
  3. Process the refund on your payment provider.
  4. Update the PaymentRefund status (succeeded, failed).
Request Parameters
// POST refund_url (request)
{
  "booqable_id": "a3f1c9e2-4b7d-4e8a-9c2b-1f5e6d7a8b9c",
  "charge_id": "original_charge_id",
  "total_in_cents": 5000,
  "deposit_in_cents": 1000,
  "currency": "USD"
}
Parameter Description
booqable_id The unique identifier of this PaymentRefund in Booqable's system
charge_id Your internal charge ID (provider_id from the original charge) being refunded
total_in_cents Refund amount in cents (e.g., 5000 = $50.00)
deposit_in_cents The portion of total_in_cents that is a deposit refund, in cents
currency Currency code for the refund (e.g., "USD", "EUR")
Response Parameters
// your refund_url responds with this (response)
{
  "id": "refund_123"
}
Parameter Description
id Unique identifier for the refund, in your payment app's system

Notes

Embedded Pages

Embedded pages let your app present a full custom interface directly inside the Booqable UI, loaded in an iframe. This is the right choice when your app needs a dedicated page or dashboard of its own. For example:

How embedded pages work

// booqable.json
{
  "main_page": {
    "iframe": { "url": "https://my-app.com/dashboard" }
  }
}

The url is either an absolute URL string, or a relative object like { "relative": "/dashboard" } that is resolved against your manifest's base_url (required for relative URLs). Booqable embeds it in an iframe and communicates with it over a postMessage protocol described below.

The exact same mechanism is also available as a configuration_page.iframe, an alternative to declarative content blocks for your app's settings page. See Configuration for that specific field; everything below (the auth token, the message protocol) applies identically to both.

Authentication token

The iframe URL includes a token query parameter: a JWT signed with HS256 using your app's OAuth client secret. Its payload contains:

This token doesn't expire. Treat it as a long-lived credential: validate it on every request rather than assuming freshness.

Message protocol

{
  eventName: "MESSAGE_TYPE",
  payload: {
    // Message-specific data
  }
}

The iframe and Booqable communicate through a standardized postMessage protocol. All messages follow this structure and contain two fields: eventName and payload.

Messages from iframe to Booqable

SET_IFRAME_HEIGHT Sets the iframe height in pixels.

window.parent.postMessage({
  eventName: "SET_IFRAME_HEIGHT",
  payload: {
    height: 600
  }
}, "*")

SET_FLASH_MESSAGE Displays a success or error message to the user.

window.parent.postMessage({
  eventName: "SET_FLASH_MESSAGE",
  payload: {
    type: "success", // or "error"
    message: "Configuration saved"
  }
}, "*")

REFRESH_SUBSCRIPTION Requests Booqable to refresh the app subscription data.

window.parent.postMessage({
  eventName: "REFRESH_SUBSCRIPTION",
  payload: {}
}, "*")

GET_AVATAR Requests user avatar information for display in the iframe.

window.parent.postMessage({
  eventName: "GET_AVATAR",
  payload: {
    id: "user_id",
    name: "User Name"
  }
}, "*")

NAVIGATE Requests Booqable to navigate to a different page.

The url must be a same-origin internal path (e.g. /app-store/installed). For security, Booqable resolves the URL against its own origin and ignores anything that points to a different origin, as well as javascript: and data: schemes. Use this to move the user between pages within the Booqable dashboard, not to redirect them to external sites.

The optional reload flag controls how the navigation happens:

window.parent.postMessage({
  eventName: "NAVIGATE",
  payload: {
    url: "/app-store/installed",
    reload: false
  }
}, "*")

Messages from Booqable to iframe

SET_AVATAR Provides avatar information in response to a GET_AVATAR request.

// Sent by Booqable to the iframe
{
  eventName: "SET_AVATAR",
  payload: {
    avatar: {
      id: "user_id",
      color: "#FF5733",
      initials: "UN",
      initialsColor: "#FFFFFF"
    }
  }
}

Implementation example

To the right is an example of a configuration interface.

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>App Configuration</title>
</head>
<body>
  <h1>Configure Your App</h1>
  <form id="settings-form">
    <label for="api_key">API Key</label>
    <input type="password" id="api_key" name="api_key" required>
    <div id="api_key_error" style="display: none; color: red;"></div>

    <button type="submit">Save Configuration</button>
  </form>

  <script>
    // Security: validate message origin. Match the domain or a subdomain of
    // it, never just a string suffix: "evilbooqable.com".endsWith("booqable.com")
    // is true, so a plain endsWith check here would accept a look-alike domain.
    const ALLOWED_DOMAINS = ['booqable.com'];

    function isValidOrigin(origin) {
      try {
        const hostname = new URL(origin).hostname;
        return ALLOWED_DOMAINS.some(domain => hostname === domain || hostname.endsWith('.' + domain));
      } catch {
        return false;
      }
    }

    function showError(fieldId, message) {
      document.getElementById(fieldId + '_error').textContent = message;
      document.getElementById(fieldId + '_error').style.display = 'block';
    }

    function adjustHeight() {
      window.parent.postMessage({
        eventName: 'SET_IFRAME_HEIGHT',
        payload: { height: document.body.scrollHeight }
      }, '*');
    }

    // Listen for messages from Booqable
    window.addEventListener('message', function(event) {
      if (!isValidOrigin(event.origin)) {
        console.warn('Unauthorized origin:', event.origin);
        return;
      }

      if (event.data.eventName === 'SET_AVATAR') {
        console.log('Received avatar:', event.data.payload.avatar);
      }
    });

    // Handle form submission
    document.getElementById('settings-form').addEventListener('submit', function(e) {
      e.preventDefault();

      const apiKey = document.getElementById('api_key').value.trim();
      if (!apiKey) {
        showError('api_key', 'API key is required');
        return;
      }

      // Save configuration (replace with actual API call)
      setTimeout(() => {
        // Notify Booqable of success
        window.parent.postMessage({
          eventName: 'SET_FLASH_MESSAGE',
          payload: {
            type: 'success',
            message: 'Configuration saved'
          }
        }, '*');

        window.parent.postMessage({
          eventName: 'REFRESH_SUBSCRIPTION',
          payload: {}
        }, '*');
      }, 1000);
    });

    // Adjust height on load
    window.addEventListener('load', adjustHeight);
  </script>
</body>
</html>

Configuration

Every app's settings page is built one of two ways: declarative content blocks, or an embedded iframe. Plan pricing and locale files are also covered here.

Content blocks

// booqable.json
{
  "configuration_page": {
    "content": [
      {
        "type": "Form",
        "title": { "localization_key": "form.title" },
        "fields": [
          { "name": "api_key", "type": "text", "label": { "localization_key": "form.api_key.label" }, "required": true }
        ]
      }
    ]
  }
}

Content blocks let you declaratively define the content of your settings page. If your app only needs a few simple fields, this is the way to go.

Block types

Field types for Form: text, textarea, password, email, phone, number, range, color, code, contentEditor, datepicker, select, checkbox, checkboxGroup, radioGroup, buttonGroup.

Any block can be shown conditionally with if: configured (only once the app is configured), unconfigured (only until then). An app counts as configured once all of its required form fields have a value, which is immediately if it has none. If omitted the block is always shown.

Embedded settings page

configuration_page.iframe replaces content blocks with a full embedded page, using the exact same mechanism (manifest shape, auth token, postMessage protocol) as Embedded Pages. See that section for the details.

Plans

Plan pricing and store-facing copy are declared directly in booqable.json's plans array. See App Store Listing for the field reference. There's no separate plans file.

Locale files

// hardcoded
{ "title": "Gizmo App" }
// localized
{ "title": { "localization_key": "app.title" } }

A lot of the strings in booqable.json (titles, descriptions, labels, banner text, and so on) accept either a hardcoded value, or a reference to a translated string.

Once you reference a localization_key, the actual string lives in a locale file instead: one YAML file per locale (see App Manifest).

# config/locales/en.yaml
en:
  app:
    title: "Gizmo App"
    description:
      short: "A simple app to manage your gizmos."
      long: "Manage your gizmos, and delight your customers with new ones every month."
  form:
    api_key:
      label: "API Key"
// booqable.json
{
  "app_store": {
    "title": { "localization_key": "app.title" }
  }
}

There's no fixed schema inside, as long as the root key matches the locale code (e.g. en, pt_BR). The nested key path below that is entirely up to you: whatever localization_key you reference must resolve to a matching key in your locale file. A common convention, matching the schema's own examples, is to namespace by section.

If a locale is missing a key your manifest references, provisioning fails with a validation error.

Booqable Interop

The two sections below only apply if your app has its own backend: OAuth lets it call Booqable's API on a user's behalf, and Webhooks let it react to events without polling.

OAuth

Booqable apps can use OAuth authentication to securely connect with Booqable's API on behalf of the user, without requiring them to share their credentials directly.

How OAuth works in Booqable apps

When an app has OAuth authentication enabled, Booqable handles the OAuth flow on behalf of the app:

  1. The user clicks to install the app.
  2. Booqable shows the user an in-app dialog listing the permissions (scopes) your app is requesting, and asks them to grant access.
  3. If access is granted, Booqable redirects the user's browser to your app's redirect_url with an authorization code.
  4. Your app exchanges the authorization code for an access and refresh token, and can then make API calls to Booqable's API.

Configuring OAuth in your app

// booqable.json
{
  "base_url": "https://my-app.com",
  "oauth": {
    "redirect_url": "/oauth/callback",
    "scopes": ["full_access"]
  }
}

To enable OAuth for your app, configure the oauth object in your booqable.json file.

Webhooks

Apps can subscribe to webhook events to receive real-time notifications when certain actions occur in Booqable. This allows your app to respond to user actions and maintain synchronization with the platform.

To learn more about how to use webhooks see the Webhook Endpoints documentation.

Available app events

There are four webhook events specifically related to app lifecycle:

app.installed

Triggered when a user installs your app from the Booqable App Store.

app.configured

Triggered every time the subscription is saved while the app is configured, not only on the one-time transition into being configured. If a merchant edits already-configured settings again later, this fires again.

app.plan_changed

Triggered when a user upgrades or downgrades their app subscription plan.

app.uninstalled

Triggered when a user removes your app from their Booqable account.

Webhook payload example

// app.installed payload
{
  "id": "22799991-8bc7-4823-a4d1-7eb6329d21b2",
  "created_at": "2026-08-02T11:12:18.145359+00:00",
  "updated_at": "2026-08-02T11:12:18.145359+00:00",
  "event": "app.installed",
  "version": 4,
  "resource_type": "app_subscriptions",
  "data": {
    "id": "5d0c1f7a-93be-4a62-b0f5-6c1e2d8a4b37",
    "created_at": "2026-08-02T11:12:18.011273+00:00",
    "updated_at": "2026-08-02T11:12:18.011273+00:00",
    "identifier": "mailchimp",
    "app_id": "a2a94184-00f3-424f-bc36-c46a84eb1461",
    "plan_id": "18ad296f-16af-4172-a3de-44bd1218543f",
    "categories": ["marketing"],
    "price_in_cents": 1000,
    "paid": true,
    "free_during_beta": false,
    "configured": false,
    "name": "Mailchimp",
    "provider_name": "Booqable",
    "settings_values": {},
    "configuration": { /* your manifest, with references resolved */ },
    "features": {},
    "oauth_status": "pending",
    "theme_blocks": []
  }
}

All app webhook events share this envelope: event, version, and resource_type describe what happened, and the actual subscription fields are nested under data. Only event varies between app.installed, app.configured, app.plan_changed, and app.uninstalled.

Reference

booqable.json

Schema for the configuration file that defines Booqable apps.

{
  "$schema": "https://developers.booqable.com/schemas/app-0-3-0.json#",
  "id": "liquid_conf",
  "version": "1.0.0",
  "name": "MyApp",
  "author": "Booqable",
  "app_store": { /* AppStoreInfo */ },
  "plans": [ /* ... */ ],
  "configuration_page": { /* ConfigurationPage */ },
  "main_page": { /* MainPage */ },
  "order_page": { /* OrderPage */ },
  "customer_page": { /* CustomerPage */ },
  "product_page": { /* ProductPage */ },
  "status": { /* AppStatus */ },
  "tracking_script": { /* TrackingScript */ },
  "theme_blocks": [ /* ... */ ],
  "oauth": { /* OauthConfig */ },
  "base_url": "https://example.com",
  "payments": { /* PaymentsConfig */ },
  "required_apps": [ /* ... */ ],
  "lifecycle": { /* Lifecycle */ }
}
Name Type Required Description
$schema constant: "https://developers.booqable.com/schemas/app-0-3-0.json#" true Schema version for the app configuration.
id string true Unique identifier for the app, used internally.
version string true Version number or status of the app.
name string true Internal name for the app, not shown to end users.
author string true Name of the app developer or company.
app_store AppStoreInfo true
plans array of AppPlan false Subscription plans for the app.
configuration_page ConfigurationPage false Defines a UI for configuring the app.
main_page MainPage false An embedded page for the daily operations of the app.
order_page OrderPage false Extends the order page in the back office.
customer_page CustomerPage false Extends the customer page in the back office.
product_page ProductPage false Extends the product page in the back office.
status AppStatus false Options controlling the app status reported inside Booqable.
tracking_script TrackingScript false Settings for apps that provide tracking scripts meant to be installed in store pages.
theme_blocks array of ThemeBlock false Website blocks provided by this app for use in the website editor.
oauth OauthConfig false OAuth integration for apps that require Booqable to grant access on behalf of the user.
base_url string false Base URL to use when providing relative URLs elsewhere in this configuration file.
payments PaymentsConfig false Payment integration for apps that process charges, authorizations, and refunds.
required_apps array of RequiredApp false Other Booqable apps that must be installed before this app can be installed. The app store shows the requirement (and the required app's price) up front and blocks installation until it is met.
lifecycle Lifecycle false Customizes what merchants see at moments of the app's install lifecycle.

AppStoreInfo

Information about the app to display in the app store.

{
  "title": { /* LocalizableString */ },
  "description": { /* AppStoreDescription */ },
  "categories": [ /* ... */ ],
  "icon": { /* AssetReference */ },
  "features": [ /* ... */ ],
  "website": { /* Link */ },
  "youtube": "4tDyXghOQbI",
  "setup_minutes": 2,
  "support": { /* AppStoreSupport */ }
}
Name Type Required Description
title LocalizableString true
description AppStoreDescription true
categories array of one of: accessibility, accounting, crm, fulfillment, maps, marketing, payment, social, tracking, translation, website, widget false Category classification for organizing apps in the app store.
icon AssetReference true
features array of LocalizableString false Bulleted list of key features shown on the app's store page.
website Link false Product or marketing website link shown in the app's store page.
youtube string false YouTube video ID for an embedded overview/promo video to be shown in the app's store page.
setup_minutes integer false Estimated time in minutes to install and set up the app.
support AppStoreSupport false

AppStoreDescription

Short and long copy for the app store listing.

{
  "short": { /* LocalizableString */ },
  "long": { /* LocalizableString */ }
}
Name Type Required Description
short LocalizableString true
long LocalizableString true

AppStoreSupport

How to get help with the app.

{
  "email": { /* LocalizableString */ },
  "website": { /* LocalizableString */ },
  "youtube": "4tDyXghOQbI"
}
Name Type Required Description
email LocalizableString false
website LocalizableString false
youtube string false YouTube video ID for an embedded support video.

RequiredApp

Another Booqable app this app depends on.

{
  "id": "deliveries",
  "description": { /* LocalizableString */ }
}
Name Type Required Description
id string true Identifier of the required app, matching its own id.
description LocalizableString false Explains why the app is required. Falls back to Booqable's generic copy when omitted.

Lifecycle

Hooks into moments of the app's install lifecycle. Each hook customizes what Booqable shows merchants at that moment.

{
  "post_install": { /* LifecycleHook */ }
}
Name Type Required Description
post_install LifecycleHook false Shown right after the app has been installed. Use it to point merchants at setup steps the app still needs.

LifecycleHook

{
  "message": { /* LifecycleMessage */ }
}
Name Type Required Description
message LifecycleMessage false

LifecycleMessage

Copy shown to the merchant. Each field falls back to Booqable's generic copy when omitted.

{
  "heading": { /* LocalizableString */ },
  "body": { /* LocalizableString */ },
  "action": { /* ... */ }
}
Name Type Required Description
heading LocalizableString false
body LocalizableString false
action object false

AppPlan

Subscription plan (e.g. free, pro) with price and app-store copy.

{
  "id": "free",
  "price_in_cents": 42,
  "app_store": { /* AppPlanStoreInfo */ },
  "features": [ /* ... */ ],
  "paid_during_beta": true,
  "most_popular": true
}
Name Type Required Description
id string true
price_in_cents integer true
app_store AppPlanStoreInfo true
features array of PlanFeature false
paid_during_beta boolean false When true, plan remains paid during beta; when false, plan is free during beta.
most_popular boolean false When true, this plan is marked as most popular in the app store.

AppPlanStoreInfo

Per-plan title, description, and feature list shown in the app store.

{
  "title": { /* LocalizableString */ },
  "description": { /* LocalizableString */ },
  "features": [ /* ... */ ]
}
Name Type Required Description
title LocalizableString true
description LocalizableString false
features array of LocalizableString false

PlanFeature

Feature flag for a plan (id and whether it is enabled).

{
  "id": "lorem ipsum",
  "enabled": true
}
Name Type Required Description
id string true
enabled boolean true

MainPage

An embedded page for the daily operations of the app.

{
  "iframe": { /* Iframe */ },
  "if": { /* RenderCondition */ }
}
Name Type Required Description
iframe Iframe true
if RenderCondition false

Iframe

An embedded iframe.

{
  "url": { /* LocatableUrl */ }
}
Name Type Required Description
url LocatableUrl true

OrderPage

Additions this app makes to the order page in the back office.

{
  "sidebar_frame": { /* SidebarFrame */ }
}
Name Type Required Description
sidebar_frame SidebarFrame true

CustomerPage

Additions this app makes to the customer page in the back office.

{
  "sidebar_frame": { /* SidebarFrame */ }
}
Name Type Required Description
sidebar_frame SidebarFrame true

ProductPage

Additions this app makes to the product page in the back office.

{
  "sidebar_frame": { /* SidebarFrame */ }
}
Name Type Required Description
sidebar_frame SidebarFrame true

SidebarFrame

An embedded panel in the page's sidebar. Each app can add one per page.

{
  "url": { /* LocatableUrl */ }
}
Name Type Required Description
url LocatableUrl true URL of the page to embed in the panel's iframe. Booqable appends signed query parameters identifying the company and the record being viewed.

AppStatus

Customize this app's status reported inside Booqable.

{
  "dynamic": { /* ... */ }
}
Name Type Required Description
dynamic object false Fetch a live status message from an HTTPS endpoint. Called directly from the browser. The endpoint must respond with an Access-Control-Allow-Origin header allowing the back-office origins, and its body must be the plain-text message.

ConfigurationPage

A list of UI blocks or an embedded page for configuring the app. Exactly one of content or iframe must be given.

One of the following:

ConfigurationPageBlock

A UI block for use in the app's configuration page.

One of the following:

RenderCondition

When to render: only when the app is configured, or only when unconfigured. Omit to always render.

"configured"

One of: configured, unconfigured

BannerBlock

Banner block: heading, text, optional link, and style (neutral/info/warning).

{
  "type": "Banner",
  "heading": { /* LocalizableString */ },
  "text": { /* LocalizableString */ },
  "link": { /* Link */ },
  "nature": "neutral",
  "if": { /* RenderCondition */ }
}
Name Type Required Description
type constant: "Banner" true
heading LocalizableString false
text LocalizableString false
link Link false
nature one of: neutral, info, warning false Banner style; default is neutral.
if RenderCondition false

Clickable link with text and URL; optionally rendered as a button.

{
  "text": { /* LocalizableString */ },
  "url": "https://example.com",
  "display_as_button": true
}
Name Type Required Description
text LocalizableString true
url string true
display_as_button boolean false

FormBlock

Form with fields whose values are saved as app settings.

{
  "type": "Form",
  "title": { /* LocalizableString */ },
  "preamble": { /* LocalizableString */ },
  "fields": [ /* ... */ ],
  "if": { /* RenderCondition */ }
}
Name Type Required Description
type constant: "Form" true
title LocalizableString true
preamble LocalizableString false
fields array of FormBlockField false
if RenderCondition false

FormBlockField

Single form field (name, input type, label, validation, and for select: options and multi).

{
  "name": "lorem ipsum",
  "type": "buttonGroup",
  "label": { /* LocalizableString */ },
  "required": true,
  "description": { /* LocalizableString */ },
  "placeholder": { /* LocalizableString */ },
  "options": [ /* ... */ ],
  "multi": true,
  "pattern": ".*\\.mydomain\\.com/.*",
  "min": 42,
  "max": 42,
  "step": 42
}
Name Type Required Description
name string true
type one of: buttonGroup, checkbox, checkboxGroup, code, color, contentEditor, datepicker, email, number, password, phone, radioGroup, range, select, text, textarea true Input type; must match a type supported by the back-office form (InputField).
label LocalizableString false
required boolean false
description LocalizableString false
placeholder LocalizableString false
options array of SelectFieldOption false Required for select type.
multi boolean false Allow multiple selection for select fields.
pattern string false Regex pattern for validating text/textarea fields. The value must match this pattern.
min number false Minimum value for number/range fields.
max number false Maximum value for number/range fields.
step number false Step increment for number/range fields.

SelectFieldOption

One option for a select or multi-select field (value and label).

{
  "value": "lorem ipsum",
  "label": { /* LocalizableString */ }
}
Name Type Required Description
value string true
label LocalizableString true

GuideBlock

Guide block: ordered steps with title, text, and optional image per step.

{
  "type": "Guide",
  "title": { /* LocalizableString */ },
  "steps": [ /* ... */ ],
  "if": { /* RenderCondition */ }
}
Name Type Required Description
type constant: "Guide" true
title LocalizableString true
steps array of GuideBlockStep false
if RenderCondition false

GuideBlockStep

One step in a guide (title, optional text, optional image).

{
  "title": { /* LocalizableString */ },
  "text": { /* LocalizableString */ },
  "image": { /* AssetReference */ }
}
Name Type Required Description
title LocalizableString true
text LocalizableString false
image AssetReference false

ThemeBlock

A website block provided by the app.

{
  "name": { /* LocalizableString */ },
  "template": { /* AssetPath */ },
  "allowed_sections": [ /* ... */ ],
  "cookies_required": true,
  "settings_iframe_url": { /* LocatableUrl */ },
  "settings": { /* ... */ }
}
Name Type Required Description
name LocalizableString true Display name for the block in the website editor.
template AssetPath true Path to the Liquid template file for the block.
allowed_sections array of one of: header, footer, app true Section types where this block may be placed.
cookies_required boolean false Display a notice in this block's settings that the block needs website visitors' cookie consent. This does not stop the block from rendering.
settings_iframe_url LocatableUrl false Embed an iframe in the website editor sidebar for this block's settings, instead of the usual controls.
settings object false Which settings are available for this block in the website editor.

ThemeBlockSetting

One setting for a theme block. Renders in the website editor sidebar.

{
  "type": "header",
  "content": { /* LocalizableString */ },
  "label": { /* LocalizableString */ },
  "placeholder": { /* LocalizableString */ },
  "default": "lorem ipsum",
  "min": 42,
  "max": 42,
  "step": 42
}
Name Type Required Description
type one of: header, divider, text, textarea, number, color, checkbox true Type of the setting field.
content LocalizableString false Text for header type.
label LocalizableString false Display label for the setting.
placeholder LocalizableString false Hint text inside the input. Applies to text, textarea.
default string \ number false
min number false A minimum value for number typed fields.
max number false A maximum value for number typed fields.
step number false A step increment for number typed fields.

TrackingScript

Settings controlling JavaScript files added to store pages for tracking purposes.

{
  "template": { /* AssetPath */ }
}
Name Type Required Description
template AssetPath true Path to the JavaScript template file for the tracking script.

AssetPath

A reference to a file bundled with the app.

{
  "asset": "templates/widget.liquid"
}
Name Type Required Description
asset string true Path to the file relative to the app root.

AssetReference

A URL to an external asset or a reference to a file bundled with the app.

One of the following:

LocatableUrl

An absolute URL string or a relative URL object resolved against base_url.

One of the following:

AbsoluteUrl

An absolute URL string.

"https://example.com"

RelativeUrl

A relative URL resolved against the app's base_url at runtime.

{
  "relative": "lorem ipsum"
}
Name Type Required Description
relative string true

LocalizableString

Plain string or a reference to a string in the app localization files.

One of the following:

PaymentsConfig

Payment integration settings.

{
  "routes": { /* PaymentRoutes */ },
  "pay_button": { /* AssetReference */ }
}
Name Type Required Description
routes PaymentRoutes true
pay_button AssetReference false Asset reference (path or URL) used as the payment button in the checkout flow, typically an SVG.

PaymentRoutes

URL paths for payment operations, resolved against base_url at runtime.

{
  "charge_url": { /* LocatableUrl */ },
  "cancel_charge_url": { /* LocatableUrl */ },
  "authorization_url": { /* LocatableUrl */ },
  "capture_authorization_url": { /* LocatableUrl */ },
  "void_authorization_url": { /* LocatableUrl */ },
  "refund_url": { /* LocatableUrl */ }
}
Name Type Required Description
charge_url LocatableUrl false
cancel_charge_url LocatableUrl false
authorization_url LocatableUrl false
capture_authorization_url LocatableUrl false
void_authorization_url LocatableUrl false
refund_url LocatableUrl false

OauthConfig

OAuth configuration for the app.

{
  "redirect_url": "/deliveries/oauth/callback",
  "scopes": [ /* ... */ ]
}
Name Type Required Description
redirect_url string true Relative path the OAuth provider redirects to after authorization.
scopes array of one of: full_access true OAuth scopes the app requests.