Metadata
Every major resource accepts a free-form metadata field
— a JSON object you attach to capture your own business state. Throttle stores it as jsonb, returns it on every read, and emits it in
webhook payloads. Outside documented automation keys, Throttle treats it as opaque storage.
fulfillmentType for
digital delivery or access grants. Use the rest to bridge Throttle's data model with
your own — sales rep IDs, campaign codes, contract numbers, feature flags, merchant notes.
Resources that accept metadata
| Resource | Set on create | Update via PATCH | Returned on GET | In webhook payloads |
|---|---|---|---|---|
| Customers | Yes | Yes | Yes | Yes |
| Subscriptions | Yes | Yes | Yes | Yes |
| Orders | Yes | Yes | Yes | Yes |
| Payments | Yes | No | Yes | Yes |
| Carts | Yes | Yes | Yes | Yes |
| Cart line items | Yes | Yes | Yes | Yes |
| Fulfillments | Yes | No | Yes | Yes |
| Checkout sessions | Yes | No | via auth'd GET | No |
Update support is missing on payments and checkout sessions because those are short-lived single-use records — payments capture once, sessions expire in 30 minutes. If you need to mutate metadata on them, do it at create time.
Customers
// Customers — use the REST API until a focused customer SDK is published.
const create = await fetch('https://api.usethrottle.dev/api/v1/customers', {
method: 'POST',
headers: {
'x-api-key': process.env.THROTTLE_API_KEY!,
'content-type': 'application/json',
},
body: JSON.stringify({
email: '[email protected]',
externalId: 'user_42',
metadata: {
plan: 'enterprise',
cohort: 'q2-2026',
referredBy: 'partner_x',
},
}),
});
const { data: customer } = await create.json();
// Returned on every read:
const { data: fetched } = await fetch(`https://api.usethrottle.dev/api/v1/customers/${customer.id}`, {
headers: { 'x-api-key': process.env.THROTTLE_API_KEY! },
}).then((res) => res.json());
console.log(fetched.metadata.plan); // 'enterprise' Subscriptions
// Subscriptions
import { createSubscriptionsClient } from '@usethrottle/subscriptions/server';
const subscriptions = createSubscriptionsClient({
apiKey: process.env.THROTTLE_API_KEY!,
});
const now = new Date();
const end = new Date(now);
end.setMonth(end.getMonth() + 1);
const sub = await subscriptions.create({
externalCustomerId: 'user_42',
planReference: 'pro_monthly',
interval: 'monthly',
amount: 2999,
currentPeriodStart: now.toISOString(),
currentPeriodEnd: end.toISOString(),
metadata: {
salesRepId: 'rep_99',
contractId: 'con_2026_xyz',
region: 'EMEA',
},
});
// Update at any time via PATCH:
await subscriptions.update(sub.id, {
metadata: { ...sub.metadata, contractRenewedAt: '2026-05-03' },
}); Carts and line items
// Carts AND cart line items
import { CartClient } from '@usethrottle/cart';
const carts = new CartClient({
apiKey: process.env.THROTTLE_API_KEY!,
});
const cart = await carts.carts.create({
applicationId,
metadata: { source: 'mobile_app', utmCampaign: 'summer_sale' },
});
await carts.items.add(cart.id, {
type: 'subscription',
name: 'Pro Plan',
unitPrice: 2999,
quantity: 1,
metadata: {
sku: 'PRO-001',
catalogVersion: '2026-04',
fulfillmentType: 'access_grant',
resourceType: 'plan',
resourceId: 'pro_monthly',
permissions: ['read'],
requiresShipping: false,
},
}); Fulfillment hints on line items
When an order is paid and in processing, Throttle
reads line-item fulfillment hints. Digital and access-grant items are fulfilled
automatically; shipment and service items stay manual in the dashboard. Net30 invoices
wait for capture before delivery.
Cart items and order line items accept an explicit deliveryMethod field: shipment, digital, access_grant, service, in_person, custom, or none. This is the preferred way to say what an item
needs — set it directly and skip the metadata hints below entirely.
// Preferred: set delivery method explicitly, no metadata hint needed.
await carts.items.add(cart.id, {
type: 'product',
name: 'Gift card',
unitPrice: 5000,
quantity: 1,
deliveryMethod: 'digital',
});
// Resolved once at insert time and frozen on the row — later edits to
// metadata or type do not change it after the fact.
Omit deliveryMethod and Throttle resolves it once, at
insert time, from type and the metadata hints below —
then freezes the result on the row. It is not re-resolved on later updates, so changing
an item's metadata after creation does not change its delivery method.
// Digital product: auto-fulfilled after payment.
await carts.items.add(cart.id, {
type: 'product',
name: 'Design System Kit',
unitPrice: 4900,
quantity: 1,
metadata: {
sku: 'DS-KIT-2026',
fulfillmentType: 'digital',
downloadUrl: 'https://cdn.example.com/design-system-kit.zip',
downloadLimit: 3,
downloadExpiry: '2026-12-31T23:59:59.000Z',
requiresShipping: false,
},
});
// Access product: auto-granted after payment.
await carts.items.add(cart.id, {
type: 'subscription',
name: 'Pro Plan',
unitPrice: 2999,
quantity: 1,
metadata: {
fulfillmentType: 'access_grant',
resourceType: 'plan',
resourceId: 'pro_monthly',
permissions: ['read'],
},
}); | Key | Values | Effect |
|---|---|---|
| fulfillmentType | digital, access_grant, shipment, service, none | Primary hint. digital/access_grant auto-create completed fulfillments after payment. |
| downloadUrl / downloadStorageKey / licenseKey | string | Stored on the digital delivery detail row. |
| downloadLimit / downloadExpiry | number / ISO date | Stored on digital delivery. |
| resourceType / resourceId / permissions | string / string / string[] | Stored on the access grant detail row. |
| requiresFulfillment | false | Marks the item as not applicable for fulfillment. |
Defaults are conservative: unmarked products are shipments, subscriptions and tickets are
access grants, services stay manual, and donations are not applicable. For older
integrations, requiresShipping: false is treated as
digital unless requiresFulfillment: false is set.
Orders and payments
// Orders + payments
// Orders persist metadata you set on them. Auto-created from a checkout
// session inherit metadata you stamped on the cart.
await fetch(`https://api.usethrottle.dev/api/v1/orders/${orderId}`, {
method: 'PATCH',
headers: {
'x-api-key': process.env.THROTTLE_API_KEY!,
'content-type': 'application/json',
},
body: JSON.stringify({
metadata: {
fulfillmentVendor: 'shipstation',
merchantNotes: 'priority',
},
}),
});
// Payments accept metadata only at create-time today (no PATCH endpoint).
await fetch(`https://api.usethrottle.dev/api/v1/orders/${orderId}/payments`, {
method: 'POST',
headers: {
'x-api-key': process.env.THROTTLE_API_KEY!,
'content-type': 'application/json',
},
body: JSON.stringify({
amount: 2999,
currency: 'USD',
method: 'embedded',
metadata: { processedBy: 'rep_99', urgentRefund: false },
}),
}); Checkout sessions
// Checkout sessions
// Set metadata at session create time. The recurring intent is itself
// stored under metadata.recurring; you can stack additional fields
// alongside it.
import { createSubscriptionsClient } from '@usethrottle/subscriptions/server';
const subscriptions = createSubscriptionsClient({
apiKey: process.env.THROTTLE_API_KEY!,
});
await subscriptions.createCheckoutSession({
applicationId,
externalCartId,
customer: { externalCustomerId: 'user_42', email: '[email protected]' },
recurring: { plan: 'pro_monthly', interval: 'monthly', amount: 2999 },
metadata: { // sibling to recurring
landingPage: '/pricing/pro',
referralCode: 'FRIEND10',
},
returnUrl: 'https://shop.example/success',
cancelUrl: 'https://shop.example/pricing',
});
The recurring block is stored under metadata.recurring for the auto-create flow. You can
stack your own metadata alongside without conflict.
On checkout sessions
Session metadata is capped at 50 keys and a serialized payload of 10KB. Requests over either limit return 422 metadata_too_large.
When a checkout session converts into an order, Throttle merges metadata from three sources with this precedence (later sources win on key collisions):
- Cart metadata — set via
/api/v1/cartsat cart create time. - Session metadata — set via
/api/v1/checkout/sessions. Overwrites colliding keys from the cart. - Customer enrichment — Throttle stamps
customerEmailon the order metadata when the buyer's identity is known. This always wins.
recurring, customer_prefill, mode, amount, currency, externalCartId. Use top-level request fields for
these instead of stuffing them into metadata.
On webhooks
User-attached metadata rides through to outbound
webhook payloads. The following events now surface the merged order/session/subscription
metadata bag on their data.metadata field:
-
order.created— carries the merged cart < session < {customerEmail} bag. -
payment.captured— carries the parent order's metadata (same asorder.created). -
subscription.created— carries the subscription's metadata, which inherits from the originating session.
Use the metadata bag in your handler to attribute the event to your own records (campaign IDs, sales rep IDs, contract numbers) without a follow-up GET.
Conventions
- Keep keys snake_case or camelCase consistently. Throttle preserves whatever you send. Pick one and stick with it.
- Don't store secrets. Metadata is returned on GET and emitted in webhook payloads. Treat it as semi-public.
- Update via merge, not replace. When you
PATCHwithmetadata, you replace the entire object. Read the existing object, spread it, set your new keys, then PATCH. - Size limits are enforced. Keep metadata under 50 keys and 10KB. Use it for IDs and flags, not for dumping payloads.
Reserved key: subscription_id on orders
Throttle writes one metadata key of its own. Every order a subscription bills —
the signup charge, each renewal, and any plan-change charge — carries metadata.subscription_id pointing at the
subscription that generated it. It is how the dashboard links an order to its
subscription and back again.
Filter on it with the dedicated query param rather than the fuzzy search, which also matches an id sitting under any other key:
# Every order this subscription billed
curl -s "https://api.usethrottle.dev/v1/orders?subscriptionId=$SUB_ID" \
-H "X-API-Key: $THROTTLE_SECRET_KEY"
# Exact match on metadata.subscription_id. Prefer it over q=$SUB_ID, which
# substring-matches the whole metadata blob and can return unrelated orders.
If you charge a subscription's first period yourself and create the
subscription afterwards, pass orderId on POST /v1/subscriptions and Throttle stamps the
pointer on that order for you. Without it, that order stays unlinked — see
Creating Subscriptions
. Do not write subscription_id by hand, and do not overwrite it:
the dashboard and the subscriptionId filter both read it.
Dashboard exposure
Today the merchant dashboard surfaces metadata as a raw JSON viewer on order detail pages. For customers, subscriptions, and carts, use the API and SDK responses as the source of truth for now.
Next
- API Reference — full shape of every endpoint that accepts metadata.