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 sub claim 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 in stripePaymentMethodId above. A payment method saved without it (e.g. just last4/expiryDate for display purposes) will show up fine in GET /payment-methods, but Step 5 will reject it with a 422 the 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/create always checks out exactly what's currently in the authenticated user's cart - there's no items/lineItems field 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 via POST /v2/cart, since tax/shipping/totals are all computed server-side from the same cart records. If the cart is empty, this call returns a 400 telling 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:

  1. The bearer token is decoded to resolve the current user (their placedBy).
  2. The payment method is resolved/verified (explicit paymentMethodId, or the user's default) - 404 if none exists.
  3. The cart is loaded - 400 if empty.
  4. Tax and shipping are computed from Site Settings against the fee-adjusted subtotal.
  5. 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 stripePaymentMethodId on file at all → 422 ("not registered with the payment processor") - see the note in Step 2.
    • Stripe declines the charge → 402 with 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.
  6. Only once the charge succeeds: the order + its line items are created with status pending, storing the resulting stripe_payment_intent_id/stripe_payment_status: "succeeded".
  7. The cart is cleared.

Test vs. live charges: which Stripe mode is used is decided automatically by your tenant subdomain - dev/demo tenants 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: pendingorderedprocessedpartially_shipped / shippeddelivered (or cancelled at any point within the cancellation window). See Order Management API for the approve/deny/reorder/cancel endpoints.

Handling Errors

CodeMeaningWhat to do
400Missing required field, or the cart is emptyAdd items via POST /v2/cart before checking out
401Missing, invalid, or expired bearer tokenGet a new token from POST /v1/auth/token
402Stripe declined the chargeShow 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
404No matching/default payment method foundCreate one first (see Step 2)
422The payment method has no real Stripe token on fileIt was created without Stripe.js tokenization - it can't be charged as-is; create a new one with a real card
500Server errorSafe 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;
}
Previous
Messaging