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/sessionswithprotocolId, or the shortcutsPOST /v2/protocols/{protocolId}/checkout/orderand.../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/subscriptionsreturns 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
nextBillingDateby one cycle. You observe it as anorder.createdwebhook event. - A failed charge puts the subscription into
PAST_DUEdunning: the charge is retried every 3 days, a successful retry restoresACTIVE, 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 bornACTIVE), and generate due orders withPOST /v2/subscriptions/{subscriptionId}/generate-order. - When a renewal comes due, the subscription moves to
AWAITING_PARTNERand you receive asubscription.renewal_pendingwebhook 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 theX-Juniper-Signatureheader. 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-Idbut the same eventid— deduplicate on the envelope'sid, and don't assume ordering. The robust pattern is: treat an event as "something changed", thenGETthe resource for current truth. - Polling is the fallback. Every async transition is observable by reading the resource:
GET /v2/checkout/sessions/{sessionId}forstatus: "completed"andorderId,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/ordersorPOST /v2/checkout/sessionscreates a duplicate. The designed-for-retry operations areconfirm-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
checkoutUrltoken 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 theorderId, not the session. - Search is eventually consistent. The
searchparameter on list endpoints runs against an asynchronously populated index — a just-created record may briefly not match. Use a directGETfor 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.