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:
- Add an analytics/marketing script — see Tracking Scripts
- Add custom content blocks to the website builder — see Theme Blocks
- Offer custom shipping rates at checkout — see Delivery Carriers
- Process payments, authorizations, or refunds — see Payment Options
- Embed a full page or dashboard inside Booqable — see Embedded Pages
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:
title— shown on the listing card and detail page. Required.description.short— shown on the listing card. Required.description.long— shown on the detail page. Required.categories— drives listing filters and detail-page pills.icon— shown on the listing card and detail page. Required.features— bulleted list on the detail page.website— link on the detail page.youtube— embedded video on the detail page.setup_minutes— "Setup time" on the detail page.support— populates the Support tab.
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:
id— a stable identifier for the plan (e.g.free,pro). Required.price_in_cents— integer,0or more. Required.app_store.title— the plan's name on the plan picker. Required.app_store.description/app_store.features— optional store-facing copy shown on the plan picker.features— internal feature flags ({id, enabled}) your app reads to know which capabilities are active for a given subscription — distinct from the display copy above.most_popular— shows a "Most popular" pill on the plan picker.paid_during_beta— paid plans are shown as free while your manifest'sversionis"beta". Set totrueto keep this plan paid during beta. Defaults tofalse.
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" }
}
}
]
}
name: display name shown in the theme editor.template: a Liquid template, rendered as HTML wherever the merchant places the block.allowed_sections: required. The section types the block can be placed in:header,footer, orapp(body sections). The block only appears in the theme editor's picker for those sections.cookies_required: shows a notice in the theme editor that this block needs visitor cookie consent. It does not stop the block from rendering. To gate your scripts, register withBooqable.registerApp(see below).settings_iframe_url: embeds a full iframe in the website editor's sidebar for this block's settings, instead of the declarativesettingsbelow.settings: declarative sidebar controls, keyed by name (see below).
Settings types
text/textarea: single- or multi-line text input.number: numeric input (min/max/step).color: color picker.checkbox: boolean toggle.header: a section-divider label in the sidebar (usescontent).divider: a plain visual divider, no label.
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.
Consent gating
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:
- If the merchant has the cookie notice enabled in their theme,
onConsent/onDenyfire formarketing-category apps as the visitor accepts, denies, or changes their choice.essential-category apps never getonConsent/onDenycalled in this case at all, onlyinitruns for them. - If the merchant has not enabled the cookie notice,
onConsentfires automatically for every registered app regardless of category, on every page load, with zero visitor interaction.onDenyis never called in this case.
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
]
}
})
clickIdParam: the param carrying the click ID. It's kept for 30 days.extraParams: params that belong to that click, like a sub-ID. They're only forwarded together with the click they arrived with, so a new click never carries an older click's extras.
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
- It applies to your declared params that are in the current page's URL.
- If several apps declare the same param, it's captured once all of them have called this.
- It's optional. Params of apps that never call it keep being forwarded.
- Call it only once the click is actually recorded. Calling it right in
onConsent, before your library has loaded, can lose the click if the visitor leaves the page first.
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
Booqable.loadScript(url)/Booqable.unloadScript(url): load or remove an external script, deduplicated by exact URL.Booqable.jQuery(callback): runscallbackonce jQuery is available on the page aswindow.$, loading jQuery if needed. The version isn't guaranteed. If the theme already provides jQuery, you get that one.
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.
identifier: a stable key for this carrier.rates_url: where Booqable sends rate requests.tax_category_id: optional, writable. Associates this carrier with a tax category used to calculate tax on its delivery rates.
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:
distance,distance_unit(metricby default)order_amount_in_centsorigin_address,origin_coordinates,destination_address,destination_coordinatesstarts_at,stops_atproducts: an array of{ id, title, price_in_cents, quantity }token: a JWT identifying the request as coming from Booqable
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:
charge_urlcancel_charge_urlauthorization_urlcapture_authorization_urlvoid_authorization_urlrefund_url
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:
- Booqable sends a
POSTrequest to yourcharge_urlwith the payment details. - Respond with your internal charge
idand aredirect_urlfor the customer. - Booqable redirects the customer to your
redirect_urlto complete payment. - Once the customer completes payment, update the PaymentCharge status (
succeeded,failed). - Redirect the customer back to the
return_urlprovided 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:
- Booqable sends a
PUTrequest to yourcancel_charge_urlwith your charge ID in the URL path and an empty JSON body ({}). - Cancel the pending charge on your payment provider (if applicable).
- 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:
- Booqable sends a
POSTrequest to yourauthorization_urlwith the authorization details. - Respond with your internal authorization
idand aredirect_urlfor the customer. - Booqable redirects the customer to your
redirect_urlto complete the authorization. - Once the customer authorizes the hold, update the PaymentAuthorization status (
succeeded,failed). - Redirect the customer back to the
return_urlprovided 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:
- Booqable sends a
PUTrequest to yourcapture_authorization_urlwith the authorization ID in the URL path. - Respond immediately with HTTP 200 OK (the charge is saved as
startedin Booqable). - Capture the funds on your payment provider.
- 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:
- Booqable sends a
PUTrequest to yourvoid_authorization_urlwith the authorization ID in the URL path and an empty JSON body ({}). - Release the held funds on your payment provider.
- 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:
- Booqable sends a
POSTrequest to yourrefund_urlwith the refund details. - Respond immediately with your internal refund
id(the refund is saved aspendingin Booqable). - Process the refund on your payment provider.
- 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
- Every request to your routes is authenticated with a
BearerJWT in theAuthorizationheader, identifying the company. This token doesn't expire, so treat it as a long-lived credential and validate it on every request, not just accept it once. - For
charge_urlandauthorization_urlflows, always update the payment status via the Booqable API before redirecting the customer back to thereturn_url. The callback page polls the payment status and will wait indefinitely if the status is never updated. - Even when a payment fails, update the status to
failedbefore redirecting. Otherwise, the callback page polls indefinitely waiting for a status change. - Always store the
booqable_idfrom the initial request. You'll need it to update the payment status later, especially for asynchronous payment processing. - Use the correct HTTP methods for the route:
charge_url,authorization_url,refund_urluse POST to create new resources.cancel_charge_url,void_authorization_url,capture_authorization_urluse PUT to update existing resources.
- A failed request to any route (429, 500, 502, 503, or 504 response) is retried by Booqable up to twice before giving up.
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:
- Custom control surfaces and dashboards.
- Complex configuration interfaces with custom validation.
- Interactive setup wizards and onboarding flows.
- Custom data visualization and monitoring.
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:
- Company ID and Company Slug
- User Email of the current employee
- Currency settings:
currency,currency_position,currency_format - Distance Unit: the company's preferred distance unit for deliveries
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:
false(default): in-app navigation that keeps the single-page app loaded.true: a full-page reload. Use this when all data must be refetched and any stale state cleared (for example after your app uninstalls itself and its subscription no longer exists).
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
Banner: a heading, body text, and an optional link (rendered as a button withdisplay_as_button: true). Anaturestyle:neutral(default),info, orwarning.Form: renders a settings form, saved toApp::Subscription#settings_values. The saved values are also available in your template as variables named after the field, e.g.const apiKey = '{{ api_key }}'. The form has atitleandpreamble(body text above the fields). Each field has aname,type,label, and optionallyrequired,description,placeholder,min/max/step(fornumber/range), oroptions/multi(forselect).Guide: ordered steps, each with atitle, optionaltext, and optionalimage. Used for step-by-step onboarding.
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:
- The user clicks to install the app.
- Booqable shows the user an in-app dialog listing the permissions (scopes) your app is requesting, and asks them to grant access.
- If access is granted, Booqable redirects the user's browser to your app's
redirect_urlwith an authorization code. - 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.
redirect_url: the path Booqable redirects to after authorization. A relative path, prefixed with your app'sbase_url.scopes: the OAuth scopes your app requests. Currently the only scope available isfull_access.
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 |
Link
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:
{ "url": "https://example.com" }—url: URL to an external asset.{ "asset": "assets/icon.png" }—asset: Path to the file relative to the app root.
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:
- A plain
string, e.g."lorem ipsum" { "localization_key": "app.title" }—localization_key: Key used to look up the string in localization files.
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. |