Placing Orders
Placing Orders
A complete, step-by-step walkthrough of placing an order through the Picos Health API - from authenticating, to browsing the catalog, to building a cart, to placing the order and checking its status afterward.
All requests below assume you've already completed API Authentication and have a valid bearer token stored in a token variable.
Step 1: Query Users
Most integrations place orders on behalf of a specific user (a patient or a facility contact). Look up the user first so you have their id for the payment method and recipient lookups below.
const findUserByEmail = async (token, email) => {
const response = await fetch(
`https://dev.api.picoshealth.com/v1/users?where={"email":{"equals":"${email}"}}`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
const { docs } = await response.json();
return docs[0] || null;
};
Your bearer token already identifies a single user (decoded from the
subclaim embedded when you exchanged your API key). Cart and order-placement calls always act on that resolved user automatically - looking the user up here is only needed if you want to display their name/email or look up their payment methods and isn't required to place the order itself.
Step 2: Query Payment Methods
An order needs a payment method on file for the user - either passed explicitly, or left out to fall back to their default (see Step 5). List their saved payment methods first to see what's available:
const getUserPaymentMethods = async (token, userId) => {
const response = await fetch(
`https://dev.api.picoshealth.com/v1/payment-methods?where={"user":{"equals":"${userId}"}}`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
const { docs } = await response.json();
return docs;
};
Creating One If None Exist
If the list comes back empty, create one before placing an order - there's nothing to fall back to otherwise. Set isDefault: true so it becomes the automatic fallback used when paymentMethodId is omitted at checkout:
const createPaymentMethod = async (token, userId, card) => {
const response = await fetch('https://dev.api.picoshealth.com/v1/payment-methods', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
user: userId,
type: 'credit_card',
last4: card.last4,
expiryDate: card.expiryDate,
stripePaymentMethodId: card.stripePaymentMethodId,
isDefault: true,
}),
});
return response.json();
};
const getOrCreatePaymentMethod = async (token, userId, card) => {
const existing = await getUserPaymentMethods(token, userId);
if (existing.length > 0) {
return existing.find((pm) => pm.isDefault) || existing[0];
}
return createPaymentMethod(token, userId, card);
};
This is the step that actually makes a payment method chargeable. Raw card numbers are never sent to this endpoint - the card must be tokenized client-side first (Stripe.js/Elements), which returns a Stripe PaymentMethod id (
pm_...). That id is what goes instripePaymentMethodIdabove. A payment method saved without it (e.g. justlast4/expiryDatefor display purposes) will show up fine inGET /payment-methods, but Step 5 will reject it with a422the moment you try to check out with it - there's nothing on file for Stripe to actually charge. See Payment Methods for the full create/update/delete reference.
Step 3: Query Products
Browse or search the catalog to find what you want to order. Every product includes a variants array with the specific SKU/unit-of-measure/price combinations available for purchase.
const searchProducts = async (token, query, limit = 20) => {
const response = await fetch(
`https://dev.api.picoshealth.com/v1/products?search=${encodeURIComponent(query)}&limit=${limit}`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
const { docs } = await response.json();
return docs;
};
Response shape (abbreviated):
{
"docs": [
{
"id": "66e7d74f-0da4-4705-9ccb-5e8f03273596",
"title": "Multi-Purpose Sharps Container with Hinged Rotor Lid 3 Gallon",
"variants": [
{ "sku": "688527R-EA-1", "uom": "EA", "qty": "1", "currency": "USD", "price": "9.2375" },
{ "sku": "688527R-CA-10", "uom": "CA", "qty": "10", "currency": "USD", "price": "92.3750" }
]
}
]
}
variants[].price already has the Picos Processing Fee applied - it's the real price the user pays, so display it as-is.
Step 4: Select a Variant and Add It to the Cart
Pick the sku of the variant you want (e.g. the single-unit EA variant vs. the case-of-10 CA variant) and add it to the current user's cart. The cart is server-side and scoped to the user resolved from your bearer token.
const addToCart = async (token, productId, sku, quantity = 1) => {
const response = await fetch('https://dev.api.picoshealth.com/v2/cart', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ productId, sku, quantity }),
});
return response.json();
};
Repeat for each product/variant the user wants. Adding the same productId + sku again increments the existing line's quantity instead of creating a duplicate. Review the cart (and its running totals) at any time:
const getCart = async (token) => {
const response = await fetch('https://dev.api.picoshealth.com/v2/cart', {
headers: { Authorization: `Bearer ${token}` },
});
return response.json();
// -> { items: [...], subtotal, tax, shipping, total }
};
Remove an item, or clear the cart entirely, if the user changes their mind:
const removeCartItem = async (token, cartItemId) => {
await fetch(`https://dev.api.picoshealth.com/v2/cart/${cartItemId}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` },
});
};
const clearCart = async (token) => {
await fetch('https://dev.api.picoshealth.com/v2/cart', {
method: 'DELETE',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ clear: true }),
});
};
Step 5: Place the Order
Once the cart has everything the user wants, check it out into an order. This is the step that actually charges the card - the order total is charged to Stripe synchronously, before anything is saved, so a declined card fails the request immediately instead of creating an unpaid order.
Why isn't cart/line-item data in this request body? It's intentionally left out. The cart already lives server-side (everything you added in Step 4), so
POST /v1/orders/createalways checks out exactly what's currently in the authenticated user's cart - there's noitems/lineItemsfield to fill in, and any such field would be ignored even if sent. This prevents a client from smuggling in different prices or quantities than what was actually added viaPOST /v2/cart, since tax/shipping/totals are all computed server-side from the same cart records. If the cart is empty, this call returns a400telling you to add items first.
paymentMethodId is optional - if you omit it, the user's default payment method (isDefault: true) is used automatically; if they have no payment methods at all, the call fails with a 404 (see Step 2).
const placeOrder = async (token, shipping, paymentMethodId = undefined) => {
const response = await fetch('https://dev.api.picoshealth.com/v1/orders/create', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
...(paymentMethodId ? { paymentMethodId } : {}), // omit to use the user's default payment method
shippingAddress: shipping.address,
shippingCity: shipping.city,
shippingState: shipping.state,
shippingZip: shipping.zip,
recipientName: shipping.recipientName,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to place order');
}
return response.json();
// -> { success, orderId, sourceId, status: "pending", subtotal, tax, shipping, total, stripePaymentIntentId, createdAt }
};
What happens server-side, in order:
- The bearer token is decoded to resolve the current user (their
placedBy). - The payment method is resolved/verified (explicit
paymentMethodId, or the user's default) -404if none exists. - The cart is loaded -
400if empty. - Tax and shipping are computed from Site Settings against the fee-adjusted subtotal.
- The charge happens now. The payment method's Stripe PaymentMethod id is attached to (or verified against) the user's Stripe customer, and a PaymentIntent is created for the full total (
off_session: true, confirm: true).- No
stripePaymentMethodIdon file at all →422("not registered with the payment processor") - see the note in Step 2. - Stripe declines the charge →
402with Stripe's own decline message (e.g."Your card was declined."). - Either way, nothing else happens - no order, no line items, and the cart is left exactly as it was so you can retry with a different payment method.
- No
- Only once the charge succeeds: the order + its line items are created with status
pending, storing the resultingstripe_payment_intent_id/stripe_payment_status: "succeeded". - The cart is cleared.
Test vs. live charges: which Stripe mode is used is decided automatically by your tenant subdomain -
dev/demotenants charge against Stripe test mode (safe to use Stripe's test tokens/cards), every other tenant charges live mode. You never choose this yourself, and there's no way to accidentally charge a live card from a dev/demo tenant or vice versa.
See Placing an Order Through the API for the full field reference and HTTP status code table.
Step 6: Check Order Status
Use the order's id (or sourceId) from Step 5's response to look it up at any time:
const getOrder = async (token, orderId) => {
const response = await fetch(
`https://dev.api.picoshealth.com/v1/orders/view/${orderId}`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
return response.json();
// -> { id, source_id, status, amount, subtotal_amount, tax_amount, shipping_amount,
// stripe_payment_intent_id, stripe_payment_status, lineItems: [...], ... }
};
Or list all of a user's orders, optionally filtered by status:
const getOrdersByStatus = async (token, status) => {
const response = await fetch(
`https://dev.api.picoshealth.com/v1/orders/view?status=${status}&sort=-createdAt`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
return response.json();
};
Status values you'll see over an order's lifecycle: pending → ordered → processed → partially_shipped / shipped → delivered (or cancelled at any point within the cancellation window). See Order Management API for the approve/deny/reorder/cancel endpoints.
Handling Errors
| Code | Meaning | What to do |
|---|---|---|
400 | Missing required field, or the cart is empty | Add items via POST /v2/cart before checking out |
401 | Missing, invalid, or expired bearer token | Get a new token from POST /v1/auth/token |
402 | Stripe declined the charge | Show Stripe's message to the user (error field) and let them pick/add a different payment method - the cart is untouched, so nothing needs to be re-added |
404 | No matching/default payment method found | Create one first (see Step 2) |
422 | The payment method has no real Stripe token on file | It was created without Stripe.js tokenization - it can't be charged as-is; create a new one with a real card |
500 | Server error | Safe to retry; if it persists, the cart is unaffected so no data is lost |
Full Flow at a Glance
async function placeOrderFlow(apiKey, { email, sku, productId, shipping }) {
// 1. Authenticate
const { token } = await fetch('https://dev.api.picoshealth.com/v1/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ apiKey }),
}).then((r) => r.json());
// 2. Find the user (optional - for display/lookups only)
const user = await findUserByEmail(token, email);
// 3. Get (or create) a payment method on file for that user - stripePaymentMethodId
// must come from Stripe.js/Elements card tokenization (see Step 2's note)
const paymentMethod = await getOrCreatePaymentMethod(token, user.id, {
last4: '4242',
expiryDate: '12/2028',
stripePaymentMethodId: 'pm_...',
});
// 4. Browse products, pick a variant, add it to the cart
await addToCart(token, productId, sku, 1);
// 5. Place the order - paymentMethodId can be omitted to use the default instead.
// This is where the card is actually charged; a decline throws here (402) with
// Stripe's message, and the cart is left untouched for a retry.
const order = await placeOrder(token, shipping, paymentMethod.id);
// 6. Success is the 201 response itself - stripePaymentIntentId confirms the charge posted
console.log(`Order ${order.sourceId} placed - total $${order.total} (${order.stripePaymentIntentId})`);
// 7. Poll for status later
const latest = await getOrder(token, order.orderId);
return latest;
}