Recipe: Create a Store, Product, and Price
The live catalog model — stores, products, plans, and prices, discoverable and checkout-referenceable by buyer agents
Create a Store, Product, and Price
BDT-223 shipped and is deployed to production. Every endpoint and request/response shape on this page is callable today against https://api.plaidly.io.
Resource model
The catalog is four resource kinds, each versioned and independently lifecycle-managed:
| Resource | Purpose |
|---|---|
store | Top-level container for a merchant's sellable catalog (a merchant may run more than one store). Does not version — mutates in place regardless of status. |
product | A sellable thing (maps to what a customer sees). Belongs to a store. |
plan | A purchasable variant of a product (e.g. "Monthly"). Belongs to a product. Carries an optional freeform entitlement object describing what the buyer gets (quota/seats/duration etc) — this is catalog metadata only, not a runtime entitlement-tracking system. |
price | A specific amount + currency/token for a plan, versioned. Belongs to a plan. |
There is no separate entitlement resource, endpoint, or lifecycle. entitlement is a plain object field on plan — the earlier locked design's five-resource-kind framing did not survive implementation; treat any reference elsewhere to a distinct entitlement resource/API as stale.
Lifecycle
product, plan, and price each move through draft → published → archived. store mutates in place at any status (it does not version).
draft— mutable, not checkout-referenceable.published— checkout-referenceable, and from this point immutable.PATCHon a published (or checkout-referenced)product/plan/pricedoes not mutate it — it creates a new version instead: the response is201with a new resource whose id differs from the one in the URL and whosesupersedes_product_id/supersedes_plan_id/supersedes_price_idpoints at the old id. The old row is untouched.archived— no longer offered to new checkouts; existing checkout snapshots/sessions referencing it are unaffected. Archiving an already-archived resource is a no-op200. A resource can be archived from eitherdraftorpublished.
This immutable-once-published rule is why prices are versioned resources rather than mutable fields on a plan — a checkout that already captured a price_id must keep working even if the merchant changes their pricing later.
DELETE (hard delete) is only legal for a draft, never-published resource with no checkout reference chain; otherwise use archive.
All four POST (create) endpoints below accept an optional Idempotency-Key header — retrying with the same key and an identical payload within 24 hours returns the original resource with 201; the same key with a different payload returns 409. See Idempotency.
Build the catalog
All of the following require merchant auth (X-API-Key or a bearer token).
# 1. Create a store
curl -X POST https://api.plaidly.io/v1/stores \
-H "X-API-Key: your_api_key" \
-H "Idempotency-Key: store-attempt-1" \
-H "Content-Type: application/json" \
-d '{
"name": "Agent Quickstart Store",
"slug": "agent-quickstart-store",
"description": "Storefront for agent-purchasable products"
}'
# -> 201 CatalogStore { "id": "store_01HX9K...", "status": "draft", ... }
# 2. Create a product, referencing the store by id in the body
curl -X POST https://api.plaidly.io/v1/products \
-H "X-API-Key: your_api_key" \
-H "Idempotency-Key: product-attempt-1" \
-H "Content-Type: application/json" \
-d '{
"store_id": "store_01HX9K...",
"name": "Pro Plan Access",
"slug": "pro-plan-access",
"description": "Monthly agent quota",
"fulfillment_type": "subscription"
}'
# -> 201 CatalogProduct { "id": "prod_01HX9K...", "status": "draft", ... }
# 3. Create a plan, referencing the product by id in the body
curl -X POST https://api.plaidly.io/v1/plans \
-H "X-API-Key: your_api_key" \
-H "Idempotency-Key: plan-attempt-1" \
-H "Content-Type: application/json" \
-d '{
"product_id": "prod_01HX9K...",
"name": "Monthly",
"slug": "monthly",
"entitlement": { "quota_requests": 100000, "period": "monthly" }
}'
# -> 201 CatalogPlan { "id": "plan_01HX9K...", "status": "draft", ... }
# 4. Create a price, referencing the plan by id in the body
curl -X POST https://api.plaidly.io/v1/prices \
-H "X-API-Key: your_api_key" \
-H "Idempotency-Key: price-attempt-1" \
-H "Content-Type: application/json" \
-d '{
"plan_id": "plan_01HX9K...",
"amount_subunits": "25000000",
"currency_or_token": "USDC",
"allowed_payment_methods": ["solana:mainnet:USDC", "ethereum:mainnet:USDC"]
}'
# -> 201 CatalogPrice { "id": "price_01HX9K...", "status": "draft", ... }Note fulfillment_type is one of one_time, subscription, metered, service. amount_subunits is a string of digits (integer smallest-unit amount, e.g. "25000000" = 25.00 USDC at 6 decimals) — check a token's decimals via Configure rails (GET /v1/payment_methods) before converting. allowed_payment_methods is optional; omitting it means no restriction beyond the merchant's rail-wide policy.
Products, plans, and prices are all created with a flat POST to their own collection (/v1/products, /v1/plans, /v1/prices) with the parent id as a body field — they are not nested under their parent's path (there is no POST /v1/stores/{store_id}/products).
Publish, leaf to root
Publishing makes a resource checkout-referenceable. A checkout intent (below) requires the price and its parent plan, product, and store to all be published and not archived — so publish everything in the chain before trying to check out against it:
curl -X POST https://api.plaidly.io/v1/stores/<STORE_ID>/publish -H "X-API-Key: your_api_key"
curl -X POST https://api.plaidly.io/v1/products/<PRODUCT_ID>/publish -H "X-API-Key: your_api_key"
curl -X POST https://api.plaidly.io/v1/plans/<PLAN_ID>/publish -H "X-API-Key: your_api_key"
curl -X POST https://api.plaidly.io/v1/prices/<PRICE_ID>/publish -H "X-API-Key: your_api_key"Each returns the updated resource with "status": "published". Publishing a product/plan/price is only legal from draft (otherwise 409); publishing an already-published store is fine (it doesn't version).
Buyer-agent discovery (no auth)
A buyer agent that only knows a merchant_id can discover published products without any credentials:
curl "https://api.plaidly.io/v1/catalog/products?merchant_id=mer_01HX9K...&sandbox=true"{
"data": [
{
"id": "prod_01HX9K...",
"store_id": "store_01HX9K...",
"merchant_id": "mer_01HX9K...",
"sandbox": true,
"name": "Pro Plan Access",
"slug": "pro-plan-access",
"fulfillment_type": "subscription",
"status": "published",
"version": 1,
"plans": [
{
"id": "plan_01HX9K...",
"name": "Monthly",
"status": "published",
"prices": [
{ "id": "price_01HX9K...", "amount_subunits": "25000000", "currency_or_token": "USDC", "status": "published" }
]
}
]
}
]
}GET /v1/catalog/products supports q (free-text ILIKE search over name/description), cursor, and limit. GET /v1/catalog/products/{product_id} returns the same nested shape for one product. Both endpoints return only published, non-archived resources — a buyer agent never sees a merchant's drafts.
Turn a discovered price into a checkout
curl -X POST https://api.plaidly.io/v1/catalog/prices/<PRICE_ID>/checkout-intent{
"checkout_reference_id": "ccs_01HX9K...",
"price_id": "price_01HX9K...",
"plan_id": "plan_01HX9K...",
"product_id": "prod_01HX9K...",
"store_id": "store_01HX9K...",
"merchant_id": "mer_01HX9K...",
"amount_subunits": "25000000",
"currency_or_token": "USDC",
"allowed_payment_methods": ["solana:mainnet:USDC", "ethereum:mainnet:USDC"],
"expires_at": "2026-07-12T13:30:00Z"
}This call is public (no auth) and requires no body beyond the price_id in the URL. It validates the price and its parent plan/product/store are all published and not archived, and records a CatalogCheckoutSnapshot — which is what flips the price/plan/product into checkout-referenced immutability, even if none of them were published-with-a-later-edit before. Once you have checkout_reference_id, hand these fields to whichever side holds the merchant's X-API-Key (the merchant's own backend, typically) to create the actual payable session with POST /v1/payment_sessions, placing checkout_reference_id in that request's metadata object — see Create or select a checkout.