Discovery
The Discovery API is the read-only "what does this tenant support, and what can be sold here?" surface. Three endpoints answer three questions:
GET /capabilities— which payment methods, currencies, and gateway integrations is the merchant set up for?GET /catalog— which products and payment pages are live, with their lifecycle rules and pricing indicators?GET /manifest— what is the full set of v1 operations available to the calling API key?
These endpoints are designed for client bootstrapping: a storefront, mobile app, or partner portal calls them once on load to render the right buttons, currency pickers, and CTA copy without having to encode tenant-specific assumptions.
All three require the read scope. Responses are deterministic per tenant + API key and are safe to cache for the duration of the session.
The machine-readable specification at /api/v1/openapi.json, and the same document at /.well-known/agent-tools.json, are public and read-only. They need no API key, accept no credentials, and are served with Access-Control-Allow-Origin: *, so browser-based tools and AI agents can fetch them directly.
Endpoints
GET /api/v1/capabilities
Scoped secret API key — send an sk_live_… value (keys issued before the typed format carry a bare pk_<hex> prefix and are also secret; they still work). A publishable pk_live_…/pk_test_… key is browser-safe, carries no scopes, and is refused by every endpoint in this spec with publishable_key_not_allowed. Coarse scopes: read, commerce, admin (admin covers everything below). Narrow per-resource scopes: journeys|pages :read|write|publish|delete, productsets :read|write|publish|delete, components|rules :read|write|delete, context :read (read-only context tier), catalog :read|write-native. publish is never implied by write.
Merchant capabilities
No content
GET /api/v1/capabilities HTTP/1.1
Host: api.peakcommerce.app
X-API-Key: YOUR_API_KEY
Accept: */*
Merchant capabilities
No content
Returns the merchant capability set: enabled paymentMethods (card, ach, sepa, paypal, …), supported currencies, and the active billing-adapter integration metadata (provider name, whether it supports refunds, whether it supports off-session charges). Use it to drive the payment-page UI without hardcoding "we support card and ACH".
supportedCurrencies is now tenant-specific. It previously returned a fixed ["USD"] for every merchant regardless of what they actually transacted in. It now reflects the merchant's own currency registry, resolved in three tiers:
the currencies an administrator has explicitly enabled for the tenant;
failing that, the distinct currencies present in the tenant's synced product catalog;
failing that,
["USD"].
Treat it as the authoritative list for rendering a currency picker. A currency absent from this list is one the merchant is not set up to accept, and a checkout attempted in it should not be offered.
Requires the read scope.
GET /api/v1/catalog
List catalog products enriched with lifecycle rules and pricing indicators, plus payment pages
Scoped secret API key — send an sk_live_… value (keys issued before the typed format carry a bare pk_<hex> prefix and are also secret; they still work). A publishable pk_live_…/pk_test_… key is browser-safe, carries no scopes, and is refused by every endpoint in this spec with publishable_key_not_allowed. Coarse scopes: read, commerce, admin (admin covers everything below). Narrow per-resource scopes: journeys|pages :read|write|publish|delete, productsets :read|write|publish|delete, components|rules :read|write|delete, context :read (read-only context tier), catalog :read|write-native. publish is never implied by write.
Enriched catalog with products and payment pages
Payment page summaries
GET /api/v1/catalog HTTP/1.1
Host: api.peakcommerce.app
X-API-Key: YOUR_API_KEY
Accept: */*
Enriched catalog with products and payment pages
{
"products": [
{
"id": "text",
"name": "text",
"sku": "text",
"description": "text",
"category": "text",
"productType": "text",
"priceCents": 1,
"currency": "text",
"billingInterval": "text",
"isActive": true,
"lifecycleRules": {
"upgrade": {},
"downgrade": {},
"cancellation": {},
"addOn": {}
},
"pricingRules": {
"hasDynamicPricing": true,
"pricingTierCount": 1
}
}
],
"paymentPages": [
{}
]
}Returns the live catalog: every active product enriched with the lifecycle rules that govern it (cancellation rule, plan-change rule, applicable add-ons), pricing indicators (hasVariablePricing, hasTieredPricing), and the list of payment pages (CMS slugs) that can sell them.
This is the right call to power a storefront or pricing page; for transactional reads (resolve a single price), use GET /pricing/resolve. For catalog editing the admin surface, use the internal catalog routes, not this endpoint.
Requires the read scope.
GET /api/v1/catalog/products
Each row carries writable: true for native internal/goods products, false for provider-synced rows and bundle parents — so an agent can tell what it may edit without attempting a write.
Scoped secret API key — send an sk_live_… value (keys issued before the typed format carry a bare pk_<hex> prefix and are also secret; they still work). A publishable pk_live_…/pk_test_… key is browser-safe, carries no scopes, and is refused by every endpoint in this spec with publishable_key_not_allowed. Coarse scopes: read, commerce, admin (admin covers everything below). Narrow per-resource scopes: journeys|pages :read|write|publish|delete, productsets :read|write|publish|delete, components|rules :read|write|delete, context :read (read-only context tier), catalog :read|write-native. publish is never implied by write.
Max items to return (1–100, default 20)
20Number of items to skip (default 0)
0Filter by product type (synced, internal, bundle, goods)
Paginated products
No content
GET /api/v1/catalog/products HTTP/1.1
Host: api.peakcommerce.app
X-API-Key: YOUR_API_KEY
Accept: */*
Paginated products
No content
Every row carries a writable flag: true for native internal and goods products, false for provider-synced rows and bundle parents.
Read it rather than attempting a write and interpreting the 409 — it tells an agent up front which parts of the catalog it may author.
Filter with ?productType= (synced, internal, bundle, goods).
POST /api/v1/catalog/products
productType is REQUIRED and must be internal or goods. Provider-owned (synced) products cannot be created, and provider / externalId / sourceIntegrationId cannot be set — those mark a row as sync-owned, and a later catalog sync could then match and overwrite it.
Scoped secret API key — send an sk_live_… value (keys issued before the typed format carry a bare pk_<hex> prefix and are also secret; they still work). A publishable pk_live_…/pk_test_… key is browser-safe, carries no scopes, and is refused by every endpoint in this spec with publishable_key_not_allowed. Coarse scopes: read, commerce, admin (admin covers everything below). Narrow per-resource scopes: journeys|pages :read|write|publish|delete, productsets :read|write|publish|delete, components|rules :read|write|delete, context :read (read-only context tier), catalog :read|write-native. publish is never implied by write.
Product created
No content
Validation error
POST /api/v1/catalog/products HTTP/1.1
Host: api.peakcommerce.app
X-API-Key: YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 159
{
"productType": "internal",
"name": "text",
"sku": "text",
"description": "text",
"category": "text",
"priceCents": 1,
"currency": "text",
"imageUrl": "text",
"isActive": true
}No content
Creates a native product — one authored in PeakCommerce rather than mirrored from a billing provider.
productType is required and must be internal or goods. It has no default here on purpose: the underlying column defaults to synced, so an omitted type would quietly mint a row claiming provider ownership that nothing upstream actually has.
provider, externalId and sourceIntegrationId are rejected. They mark a row as sync-owned, and a later catalog sync matches rows by (tenant, externalId, provider) — a native product carrying them could be matched and overwritten.
bundle products cannot be created here. A bundle is a composite parent whose children live in product_bundle_items; creating the parent alone would yield a product that resolves to nothing.
GET /api/v1/catalog/products/{id}
Scoped secret API key — send an sk_live_… value (keys issued before the typed format carry a bare pk_<hex> prefix and are also secret; they still work). A publishable pk_live_…/pk_test_… key is browser-safe, carries no scopes, and is refused by every endpoint in this spec with publishable_key_not_allowed. Coarse scopes: read, commerce, admin (admin covers everything below). Narrow per-resource scopes: journeys|pages :read|write|publish|delete, productsets :read|write|publish|delete, components|rules :read|write|delete, context :read (read-only context tier), catalog :read|write-native. publish is never implied by write.
Product, with a writable flag
No content
Not found
GET /api/v1/catalog/products/{id} HTTP/1.1
Host: api.peakcommerce.app
X-API-Key: YOUR_API_KEY
Accept: */*
No content
Notes, examples, or caveats for GET /catalog/products/{id} go here.
PATCH /api/v1/catalog/products/{id}
Refused with 409 for provider-synced rows (product_is_provider_owned) and for bundle parents (product_is_bundle). productType cannot be changed.
Scoped secret API key — send an sk_live_… value (keys issued before the typed format carry a bare pk_<hex> prefix and are also secret; they still work). A publishable pk_live_…/pk_test_… key is browser-safe, carries no scopes, and is refused by every endpoint in this spec with publishable_key_not_allowed. Coarse scopes: read, commerce, admin (admin covers everything below). Narrow per-resource scopes: journeys|pages :read|write|publish|delete, productsets :read|write|publish|delete, components|rules :read|write|delete, context :read (read-only context tier), catalog :read|write-native. publish is never implied by write.
Product updated
No content
Not found
Product is provider-owned or a bundle
PATCH /api/v1/catalog/products/{id} HTTP/1.1
Host: api.peakcommerce.app
X-API-Key: YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 85
{
"name": "text",
"description": "text",
"category": "text",
"priceCents": 1,
"isActive": true
}No content
Only native internal and goods products can be edited.
A provider-synced product is refused with 409 product_is_provider_owned. This is not a permissions quirk — a billing provider owns that row, the catalog sync upserts it by (tenant, externalId, provider), and the nightly drift watcher can trigger a sync without anyone asking. A local edit would look applied, then silently disappear, and misrepresent the provider's catalog until it did. Change it in the provider's catalog and let the sync bring it across.
A bundle is refused with 409 product_is_bundle.
productType cannot be changed — create a new product instead.
GET /api/v1/manifest
Scoped secret API key — send an sk_live_… value (keys issued before the typed format carry a bare pk_<hex> prefix and are also secret; they still work). A publishable pk_live_…/pk_test_… key is browser-safe, carries no scopes, and is refused by every endpoint in this spec with publishable_key_not_allowed. Coarse scopes: read, commerce, admin (admin covers everything below). Narrow per-resource scopes: journeys|pages :read|write|publish|delete, productsets :read|write|publish|delete, components|rules :read|write|delete, context :read (read-only context tier), catalog :read|write-native. publish is never implied by write.
Tenant capability manifest
GET /api/v1/manifest HTTP/1.1
Host: api.peakcommerce.app
X-API-Key: YOUR_API_KEY
Accept: */*
Tenant capability manifest
{
"operations": [
{}
],
"paymentPages": [
{}
],
"capabilities": {}
}Returns the full v1 operation manifest as it would apply to the calling API key — operation id, HTTP method + path, the scope it requires, and whether it is currently enabled for this tenant (some operations are gated by the active billing adapter or by a feature flag).
Useful for SDK / CLI clients that need to discover the supported surface dynamically rather than ship a baked-in OpenAPI spec.
Requires the read scope.
Rate limiting (Phase 1a — shadow mode). Every
/api/v1/*request is observed by the per-tenant governance middleware (mounted after API-key authentication). Phase 1a is shadow-only: no429responses are returned, and rate-limit / concurrency response headers are off by default. Aggregate counts are available to platform admins atGET /api/admin/governance/stats.
Last updated
Was this helpful?
