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: 'jane@example.com',
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.
// 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: 'gr4vy',
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: 'jane@example.com' },
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.
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.