Buy a bundle
const url = 'https://qinto-api.blotout.io/teams/7b8d1a52-6c1e-4b6f-9a30-2f8a54d1c3e9/bundle-purchases';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"bundleId":"2b7e4d10-8a3c-4f56-9e21-7c0d5b8f3a64","linkToken":"bl_7f3a1c92","domainIds":["3f2a9c81-5d47-4e0b-8c66-91b2a7f04d15"],"approvedCharge":{"feeCents":3900,"monthlyUsageCapCents":2000,"interval":"monthly","trialDays":14},"acceptTerms":true}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://qinto-api.blotout.io/teams/7b8d1a52-6c1e-4b6f-9a30-2f8a54d1c3e9/bundle-purchases \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "bundleId": "2b7e4d10-8a3c-4f56-9e21-7c0d5b8f3a64", "linkToken": "bl_7f3a1c92", "domainIds": [ "3f2a9c81-5d47-4e0b-8c66-91b2a7f04d15" ], "approvedCharge": { "feeCents": 3900, "monthlyUsageCapCents": 2000, "interval": "monthly", "trialDays": 14 }, "acceptTerms": true }'Buys a bundle for the team on the sites you choose, from the app store by bundleId or through a bundle link by linkToken. The purchase copies the bundle’s terms as they stand, so a later change to the bundle never reaches it. Buying installs no app: the team installs the bundle’s apps when it wants, at no charge beyond the bundle.
How it works
The caller needs manage_domain_apps on every site named, and purchase_apps on the credential to approve a charge. charge_approval_required carries charge: the fee per interval, the monthly usage cap, the interval and the free days. Repeat the call with charge as approvedCharge once a person has accepted it. acceptTerms must be true. A link purchase uses up one of the link’s purchases, also for a free bundle. Existing installs of the bundle’s apps on the chosen sites keep their settings and charge the bundle’s terms from the next UTC day. A team holds at most one active purchase of a bundle. A team API token is refused.
Required permissions
domain:manage_domain_apps:writeon all domains, or on the addressed domain when its id is in the route
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Path Parameters
Section titled “Path Parameters”Unique id (UUID) of the team that owns the addressed resources, as shown in the team settings on the Blotout platform.
Example
7b8d1a52-6c1e-4b6f-9a30-2f8a54d1c3e9Unique id (UUID) of the team that owns the addressed resources, as shown in the team settings on the Blotout platform.
Request Bodyrequired
Section titled “Request Bodyrequired”The offer, the sites to cover, the charge approval and the terms acceptance.
object
A Live public bundle from the app store. Pass this or linkToken.
Example
2b7e4d10-8a3c-4f56-9e21-7c0d5b8f3a64The token of a bundle link a developer shared. Pass this or bundleId.
Example
bl_7f3a1c92The sites the bundle covers, by id. The caller needs manage_domain_apps on each one; duplicates are collapsed.
Example
[ "3f2a9c81-5d47-4e0b-8c66-91b2a7f04d15"]The charge a person was shown, sent back as charge_approval_required gave it. Needed when the call charges. The call answers charge_approval_required with the current charge when it is missing, the fee or usage cap went up, the free days went down or the interval changed since it was shown.
object
The most the call charges per interval, in US cents: for a purchase, the price plus the extra sites past the limit; for an added site, the extra-site price.
Example
3900The most the bundle’s usage can charge in one month, in US cents, whatever the interval; 0 when it has no usage cap.
Example
2000The interval the fee is per.
Example
monthlyThe free days before the first charge; 0 when the bundle has no trial or it is over.
Example
14Example
{ "feeCents": 3900, "monthlyUsageCapCents": 2000, "interval": "monthly", "trialDays": 14}Accepts the terms and privacy policies of the bundle’s apps. Must be true.
Example
trueResponses
Section titled “Responses”The purchase.
object
A team’s holding of one bundle, with the terms copied when it bought it and the sites it chose.
object
Id of the purchase.
The team that holds it.
The bundle bought.
The bundle’s current name.
The developer organization’s name.
The bundle link the purchase came through; null from the app store.
The pricing model copied at purchase.
The price per interval copied at purchase, in US cents.
How often the price is charged.
The free days copied at purchase; null or 0 for none.
The price per interval of each site past the limit; null when none are sold.
The services copied at purchase.
A service the developer delivers, which the bundle price covers.
object
Name of the service. Empty only on a bundle that isn’t published yet.
What the team gets. Empty when not set.
What the service costs on its own, in US cents. Null only on a bundle that isn’t published yet.
How the price on its own is billed.
The most usage adds to the bill each month, in US cents.
The priced meters copied at purchase.
What one meter of one app costs on a bundle. A meter with no term is free.
object
Id of the app that reports the meter.
Key of the meter in that app. The same key in two apps is two terms.
Name of the meter on the bundle.
Plural unit, as the bill shows it. Empty only on a bundle that isn’t published yet.
Units included each month for the whole team, across its sites.
Price past the included amount, in US micro-dollars.
How many units that price covers. Null only on a bundle that isn’t published yet.
The app mode copied at purchase.
The bundle’s stored app list copied at purchase; empty for All apps.
The sites the purchase covers, A–Z by hostname.
object
Id of the site.
The site’s hostname.
Who accepted the apps’ terms.
When the terms were accepted.
Who approved the charge; null for a free bundle.
When the charge was approved; null for a free bundle.
The purchase instant. Charges start the next UTC day.
When the purchase ends: the first instant it no longer covers, still ahead while a cancel waits to take effect; null while active.
Example
{ "purchase": { "bundlePurchaseId": "9c1d2e3f-4a5b-4c6d-8e7f-0a1b2c3d4e5f", "teamId": "7b8d1a52-6c1e-4b6f-9a30-2f8a54d1c3e9", "bundleId": "2b7e4d10-8a3c-4f56-9e21-7c0d5b8f3a64", "bundleName": "Growth Stack", "publisher": "Acme Dev", "linkId": null, "pricingModel": "fixed", "basePriceCents": 2900, "billingInterval": "monthly", "trialDays": 14, "siteLimit": 3, "extraSitePriceCents": 500, "services": [ { "name": "Priority support", "detail": "4-hour response, shared Slack channel", "priceCents": 20000, "basis": "monthly" } ], "usageCapCents": 10000, "meterTerms": [ { "appId": "7f3a1c92-4b6d-4e8a-b1d3-5c9e2f7a6b10", "key": "events", "name": "Events", "unit": "events", "includedAmount": 100000, "priceMicros": 500000, "perUnits": 1000 } ], "appMode": "selected", "appIds": [], "sites": [ { "domainId": "3f2a9c81-5d47-4e0b-8c66-91b2a7f04d15", "domain": "q.shop.example.com" } ], "termsAcceptedByUserId": "1f2e3d4c-5b6a-4978-8f0e-9d8c7b6a5f4e", "termsAcceptedAt": "2026-11-08T12:00:00.000Z", "chargeApprovedByUserId": null, "chargeApprovedAt": "2026-11-08T12:00:00.000Z", "startedAt": "2026-11-08T12:00:00.000Z", "endedAt": "2026-11-08T12:00:00.000Z" }}invalid_params — A path parameter failed validation (for example a malformed UUID); details pinpoints the offending parameter. invalid_body — The JSON body is unparseable or failed schema validation; details pinpoints the offending fields when the body could be parsed.
object
Machine-readable error slug; switch on this value rather than on the HTTP status alone.
Field-level validation errors: a tree keyed by input property, each node carrying an errors array of human-readable messages. Render it to help the caller correct the request.
object
Example
{ "error": "invalid_params", "details": { "errors": [], "properties": { "teamId": { "errors": [ "Invalid UUID" ] } } }}unauthenticated — The Authorization: Bearer token is missing, malformed, or no longer valid. Generate a team API token or obtain a new OAuth access token.
object
Machine-readable error slug; switch on this value rather than on the HTTP status alone.
Example
{ "error": "unauthenticated"}no_payment_method — The team pays by card and has no payment method on file, so nothing was deployed or installed. Add a card on the team’s Billing page, then retry. team_unpaid — The team has an unpaid invoice, so it cannot install or enable an app that charges, buy a bundle, or add a site that charges. Nothing changed. Pay what is owed on the team’s Billing page, then retry.
object
Machine-readable error slug; switch on this value rather than on the HTTP status alone.
Human-readable reason for this specific failure, intended for display — do not parse it programmatically.
Example
{ "error": "no_payment_method", "message": "The Cloudflare API token is invalid or expired."}forbidden — The bearer credential lacks the required permission or belongs to another team, or its user lacks the organization role a developer operation needs. Update its policies, use a credential bound to the addressed team, or ask an organization owner for the role; message states the reason when one is known.
object
Machine-readable error slug; switch on this value rather than on the HTTP status alone.
Human-readable reason for this specific failure, intended for display — do not parse it programmatically.
Example
{ "error": "forbidden", "message": "The Cloudflare API token is invalid or expired."}not_found — The addressed resource does not exist within this team; check the id and retry.
object
Machine-readable error slug; switch on this value rather than on the HTTP status alone.
Example
{ "error": "not_found"}bundle_link_revoked — The link is revoked, which is permanent: the developer can’t change it and a team can’t buy through it. Nothing changed. The developer creates a new link instead. bundle_link_used_up — Every purchase the link allowed has been made, so the team can’t buy through it. Nothing changed. Ask the developer for a new link. bundle_not_for_sale — The bundle behind the link is Paused or Retired, or cannot be sold right now. Nothing changed. bundle_already_purchased — The team already holds this bundle. Nothing changed. A team holds at most one active purchase of a bundle. bundle_site_limit — The sites exceed the bundle’s site limit and the bundle sells no extra sites. Nothing changed. Choose fewer sites; the other sites install apps at the apps’ own prices. charge_approval_required — The call charges the team and did not approve that charge, or the charge went up, or its free days went down, after it was shown. charge holds the current charge and message states it. For an app, appSlug names the app the charge belongs to: once a person allowed to purchase apps accepts the charge and the developer’s terms, repeat the call with { "<appSlug>": <charge> } in approvedCharges, keeping the other entries. For a bundle, repeat the call with charge as approvedCharge. Nothing was installed or bought. terms_required — The purchase needs the buyer to accept the terms of the bundle’s apps. Nothing changed. Repeat the call with acceptTerms once a person has read them. subscription_item_limit — The team’s Stripe subscription holds the most lines it can, so no further app or bundle that charges can be added to it; message names the app or bundle. Nothing was installed or bought. Contact support. sites_paused — Blotout has paused this team’s sites, so nothing was deployed or installed. Every site of the team answers 410 until the pause ends, and uninstalls, switches and settings changes are still recorded. A payment does not end the pause; contact Blotout. deploys_blocked — Blotout has blocked this team’s deploys, so nothing was deployed or installed. The team’s live sites keep running, and uninstalls, switches and settings changes still roll out. A payment does not lift the block; contact Blotout.
object
Machine-readable error slug; switch on this value rather than on the HTTP status alone.
Human-readable reason for this specific failure, intended for display — do not parse it programmatically.
With charge_approval_required: the current charge to send back as approvedCharge.
object
The most the call charges per interval, in US cents: for a purchase, the price plus the extra sites past the limit; for an added site, the extra-site price.
The most the bundle’s usage can charge in one month, in US cents, whatever the interval; 0 when it has no usage cap.
The interval the fee is per.
The free days before the first charge; 0 when the bundle has no trial or it is over.
Example
{ "error": "bundle_link_revoked", "message": "The Cloudflare API token is invalid or expired.", "charge": { "feeCents": 3900, "monthlyUsageCapCents": 2000, "interval": "monthly", "trialDays": 14 }}internal — Unexpected server error. Nothing is guaranteed to have been persisted; idempotent operations are safe to retry.
object
Machine-readable error slug; switch on this value rather than on the HTTP status alone.
Example
{ "error": "internal"}