Skip to content

How It All Fits Together

The DailyStak fulfillment platform turns a practitioner's product recommendation into recurring, delivered orders. The v2 API models that as a small set of resources with clear jobs:

  • A Customer is who you are serving.
  • A Protocol is what they take — the regimen of products a practitioner assigned them.
  • A checkout session or a direct order create is the buy — how payment is collected and the first order is placed.
  • A Subscription is when it recurs — the billing cadence that generates a new order every cycle.
  • An Order is each individual shipment, moving through a fulfillment lifecycle you can observe end to end.
  • An Amendment is the consented path for changing a regimen once a subscription is live.

This guide walks the whole model once, top to bottom. Each section links to the focused guide for that resource.

graph LR
    P["Partner account<br/>(v2 credential, billing mode,<br/>branding, webhooks)"]
    C["Customer<br/><i>who</i>"]
    PR["Protocol<br/><i>what — the regimen</i>"]
    CS["Checkout session<br/><i>the buy</i>"]
    S["Subscription<br/><i>when — billing cadence</i>"]
    O["Order<br/><i>each shipment</i>"]
    A["Amendment<br/><i>consented change</i>"]

    P --> C
    C --> PR
    PR --> CS
    CS -- "payment succeeds" --> O
    CS -- "subscription mode" --> S
    S -- "every cycle" --> O
    PR -. "edits once a<br/>subscription is live" .-> A
    A -. "approved changes<br/>fold into" .-> PR

Your account and your v2 credential

Everything in the API is scoped to your partner account — your branding, billing mode, email preferences, webhook configuration, and every resource you create. Another partner's resource is indistinguishable from a missing one: you get a 404, never a 403.

You authenticate with OAuth2 client credentials using a v2 API key minted in the partner portal. v2 credentials are separate from v1: a v1 key cannot call any /v2/* endpoint, and a v2 key cannot call /v1/*. If you are migrating, see Migrating from v1.

curl -s https://auth.fulfillment.sandbox.juniperhealth.com/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET"
{
  "access_token": "eyJraWQiOiJ...",
  "expires_in": 3600,
  "token_type": "Bearer"
}

Tokens last one hour; send them as Authorization: Bearer <token>. Three scopes cover the API:

Scope Grants
order/v2.read Every read on /v2/* (orders, customers, protocols, subscriptions, checkout, settings, documents)
order/v2.write Required in addition to order/v2.read for every POST, PUT, PATCH, and DELETE
order/v2.product.read The product catalog: GET /v2/products, GET /v2/ingredient-families

Base URLs: https://api.fulfillment.juniperhealth.com (production) and https://api.fulfillment.sandbox.juniperhealth.com (sandbox). Sandbox is a full deployment with identical authentication and behavior — build there first.

Customers: referenced, not embedded

A Customer is a first-class record you create once (POST /v2/customers) — or that an order creates for you — and reference everywhere by its ID. Protocols, orders, and subscriptions all point at the same customer record, so an address update in one place is an address update everywhere that matters going forward. You can carry your own identifier in sourceId to link the record back to your system.

Customers also anchor payment: POST /v2/customers/{customerId}/billing-portal opens a Stripe billing-portal session where the customer can manage their saved payment methods — the one place the payment processor is directly visible to you.

Protocols: the WHAT

A Protocol is one customer's regimen: the product line items a practitioner assigned, with quantities and dosing schedule. It deliberately excludes shipping and billing cadence — those are chosen at checkout or on the subscription. A customer may hold several active protocols at once.

Protocols are the hub of the model. From a protocol you can:

  • mint a hosted checkout link (POST /v2/checkout/sessions with protocolId, or the shortcuts POST /v2/protocols/{protocolId}/checkout/order and .../checkout/subscription),
  • hang a subscription off it (partner-managed billing) so it renews every cycle,
  • change the regimen later through Amendments.

Protocol templates are reusable starting points you own; the DailyStak library is the curated, read-only counterpart you can copy from.

The buy: two ways to place an order

Hosted checkout (POST /v2/checkout/sessions) is the path when the platform collects payment. You create a session — from a protocolId or an explicit products list — and receive a checkoutUrl, a tokenized payment page valid for 24 hours. Your customer pays there; the platform prices the cart server-side (subtotal, discounts, tax, then shipping when the customer picks a method) and the quoted amount equals the charged amount. Completion is asynchronous: seconds after payment, the order (and in subscription mode, the subscription) is created, and the order.created webhook event is your signal. See Hosted Checkout.

Direct order creation (POST /v2/orders) skips payment collection entirely: you submit line items and shipping details, and the order goes straight into fulfillment. No charge is made by the platform — this is the path for partner-managed billing, where you bill your customer yourself and DailyStak invoices you at wholesale. See Orders.

Either way, pricing is computed and frozen at creation. The order carries a settlement snapshot — subtotals, discounts, fees, shipping, tax, and the final customerTotal — and later catalog or discount changes never touch an existing order. That frozen snapshot is also what refunds are measured against.

Subscriptions: the WHEN

A Subscription is a billing schedule that references a protocol. The protocol owns the regimen; the subscription owns the cadence: daysSupply is both the supply per shipment and the renewal interval (a 30-day supply renews every 30 days — there is no separate billing frequency, and no proration anywhere on the platform). Each cycle, line items come from the protocol as it stands at that moment, so an approved amendment naturally shows up in the next renewal. One active subscription per protocol.

Which billing mode you have

Billing mode is an account-level setting on your partner account, fixed when you onboard — it is not a per-subscription choice and cannot be changed through the API. If you are unsure which applies to you, ask your DailyStak contact. The two modes change who charges the customer:

Juniper-managed billing — the platform owns payment.

  • Subscriptions are created by hosted checkout only (POST /v2/subscriptions returns 400 and points you to checkout). The customer's card is saved at checkout for renewals.
  • Each cycle the platform charges the saved card, generates the renewal order, and advances nextBillingDate by one cycle. You observe it as an order.created webhook event.
  • A failed charge puts the subscription into PAST_DUE dunning: the charge is retried every 3 days, a successful retry restores ACTIVE, and the third failed charge cancels the subscription.

Partner-managed billing — you own payment.

  • You may create subscriptions directly with POST /v2/subscriptions (they are born ACTIVE), and generate due orders with POST /v2/subscriptions/{subscriptionId}/generate-order.
  • When a renewal comes due, the subscription moves to AWAITING_PARTNER and you receive a subscription.renewal_pending webhook event carrying a one-time confirmation token (48-hour expiry), the priced amount, and a line-item summary:
{
  "id": "evt_2zEvNtKq7VhXw3RbYcLm0PdGa5s",
  "type": "subscription.renewal_pending",
  "created": 1784988000,
  "data": {
    "subscriptionId": "2zAbC9xkQ21LmNoPqRsTuVwXyZ0",
    "customerId": "2vTgd30JFgnwLrgb383PcV9Rken",
    "protocolId": "2vSGym0bH8qVEwCIGlyFoRgJq1A",
    "confirmationToken": "2zTOKEN0abcdefghijklmnopqrs",
    "expiresAt": "2026-07-27T14:00:00+00:00",
    "amountCents": 4440,
    "lineItems": [
      { "productId": 77123129891, "name": "Elite EPA", "quantity": 2, "customerPrice": "0.74" }
    ]
  }
}
  • You charge your customer through your own billing, then confirm:
curl -X POST https://api.fulfillment.juniperhealth.com/v2/subscriptions/2zAbC9xkQ21LmNoPqRsTuVwXyZ0/confirm-renewal \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"confirmationToken": "2zTOKEN0abcdefghijklmnopqrs", "externalPaymentReference": "pmt_88123"}'
{
  "orderId": "2zORDERxyzabcdefghijklmnopq",
  "message": "Renewal confirmed, order generated",
  "idempotent": false
}

The renewal order is generated, the subscription returns to ACTIVE, and nextBillingDate advances one cycle. Confirmation is idempotent per token — a retry returns the same orderId — so it is always safe to retry after a timeout. - If a token expires unconfirmed, the subscription returns to ACTIVE and the renewal is re-offered on a later cycle; three consecutive missed windows move it to FAILED and emit subscription.payment_failed.

In both modes you can pause, resume, cancel, skip a cycle, or reschedule the next billing date through the subscription API. Note that resuming does not push nextBillingDate out — if the date passed while paused, the renewal is immediately due (reschedule first if that's not what you want).

Amendments: changing a live regimen

Once a protocol has a live subscription, direct edits to it are blocked. Regimen changes flow through Amendments: you author a draft (a batched set of add/remove/update changes), optionally price it with POST /v2/protocols/{protocolId}/amendments/cost-preview, then send it for approval. The consent gate then decides:

  • No live subscription (or you attest the customer approved offline) → the amendment is applied immediately.
  • Live subscription → the amendment goes pending, and the customer approves or rejects it through a single-use review link (emailed to them, or retrievable via the approval-link endpoint to deliver through your own channel).

Approved changes fold into the protocol and take effect on the next renewal — the customer sees the new price before it is ever charged. That is the point of the model: quote == charge, with consent in between.

The order lifecycle

Every order — whether it came from checkout, a direct create, or a renewal — moves through the same states:

stateDiagram-v2
    [*] --> NEW: order created
    NEW --> PROCESSING
    PROCESSING --> DISPENSING
    DISPENSING --> FILLED
    FILLED --> SHIPPED: carrier picks up
    SHIPPED --> DELIVERED: carrier confirms
    NEW --> CANCELLED
    PROCESSING --> CANCELLED
    NEW --> EXCEPTION: fulfillment problem
    PROCESSING --> EXCEPTION: fulfillment problem
    DISPENSING --> EXCEPTION: fulfillment problem
    FILLED --> EXCEPTION: fulfillment problem
    EXCEPTION --> PROCESSING: resolved
    DELIVERED --> [*]
    CANCELLED --> [*]
Status Meaning Webhook event
NEW Accepted; pricing frozen; queued for fulfillment order.created
PROCESSING In the fulfillment pipeline order.updated
DISPENSING Being dispensed and packed order.updated
FILLED Packed, awaiting carrier pickup order.updated
SHIPPED With the carrier; fulfillmentInfo (carrier, tracking number, tracking URL) is populated on the order order.shipped
DELIVERED Carrier confirmed delivery order.delivered
EXCEPTION A fulfillment problem needs attention; returns to PROCESSING once resolved order.exception
CANCELLED Cancelled while still NEW or PROCESSING (a partner-managed order via POST /v2/orders/{orderId}/cancel; any other by DailyStak) order.cancelled

Tracking appears on the order itself — poll GET /v2/orders/{orderId} or react to order.shipped and read fulfillmentInfo. You can update the shipping address with PATCH /v2/orders/{orderId}/address while the order is still NEW or PROCESSING. The status enum also contains FULFILLED and the deprecated AWAITING_SHIPMENT for historical orders; current fulfillment does not write either.

Refunds

Refunds are issued by DailyStak, not through the API; you read them with GET /v2/orders/{orderId}/refunds. A refund is capped by the order's frozen charged total — the amount actually collected under your billing mode at creation time, minus refunds already made. Settlement is asynchronous: creation emits refund.created, completion emits refund.completed and updates the order's refund ledger, and a failure, immediate or days later, emits refund.failed.

Integrate async-first

Everything involving money or physical goods settles asynchronously: checkout completion, order status changes, refund settlement, renewals. Design around that:

  • Webhooks are the primary signal. Configure endpoints in the partner portal; every event arrives as {"id": "evt_…", "type": "…", "created": <unix-seconds>, "data": {…}}, HMAC-signed via the X-Juniper-Signature header. See Webhooks for verification and the full catalog.
  • Delivery is at-least-once. Failed deliveries are retried with exponential backoff; each attempt carries a fresh X-Juniper-Webhook-Id but the same event id — deduplicate on the envelope's id, and don't assume ordering. The robust pattern is: treat an event as "something changed", then GET the resource for current truth.
  • Polling is the fallback. Every async transition is observable by reading the resource: GET /v2/checkout/sessions/{sessionId} for status: "completed" and orderId, GET /v2/orders/{orderId} for fulfillment progress, GET /v2/orders/{orderId}/refunds/{refundId} for settlement.

Events you will actually see, by resource:

Resource Events
Orders order.created, order.updated, order.shipped, order.delivered, order.cancelled, order.exception
Subscriptions subscription.created (direct create only), subscription.updated, subscription.paused, subscription.resumed, subscription.cancelled, subscription.skipped, subscription.rescheduled, subscription.renewal_pending, subscription.renewal_blocked, subscription.order_created, subscription.payment_failed
Customers customer.created, customer.updated, customer.deleted
Protocols protocol.created, protocol.updated, protocol.deactivated, protocol.reactivated, protocol.archived
Amendments protocol_change_request.created, .approved, .rejected, .cancelled
Refunds refund.created, refund.completed, refund.failed

Two easy-to-miss facts: there is no checkout.* event (checkout completion is signaled by order.created), and subscription.created is not emitted for checkout-created subscriptions — only for ones you create via POST /v2/subscriptions.

Good to know

  • The platform emails your customers on v2. Order confirmations, checkout receipts, subscription lifecycle and renewal notices, amendment approval requests, and refund confirmations are sent under your branding — each toggleable in Partner Settings. This is a deliberate difference from v1, where the platform never contacted your customers.
  • Wire conventions: JSON is camelCase; timestamps are ISO 8601 with a UTC offset (always send an explicit offset — 2026-08-24T00:00:00Z, never a naive datetime); money is decimal strings ("84.00") except refund amounts, which are integer cents; IDs are 27-character sortable strings (KSUIDs), except product IDs, which are integers in requests.
  • Few writes are idempotent. Retrying a successful POST /v2/orders or POST /v2/checkout/sessions creates a duplicate. The designed-for-retry operations are confirm-renewal (idempotent per token) and subscription cancel (cancelling an already-cancelled subscription returns 200); order cancel is not retry-safe. When a write times out, list or fetch before retrying.
  • Checkout URLs are secrets. The checkoutUrl token is the only credential for the payment page — don't log it, and send it only to the customer. Sessions expire after 24 hours; session records are purged roughly 90 days after expiry, so persist the orderId, not the session.
  • Search is eventually consistent. The search parameter on list endpoints runs against an asynchronously populated index — a just-created record may briefly not match. Use a direct GET for read-after-write.
  • Keep paging until the cursor is null. Filtered list pages can be short — or empty — while more results remain.

Next: work through the Quickstart, then go deep on Hosted Checkout, Subscriptions, and Webhooks.