Partner API v2 Overview¶
The DailyStak Partner API v2 gives you programmatic control over the full fulfillment lifecycle: create customers, author their supplement regimens, take payment through hosted checkout, manage recurring subscriptions, track orders through dispensing and delivery, and issue refunds — all under your own brand.
v2 is a different contract from v1, not a superset. It has its own credentials, its own resource model (customers and protocols are first-class resources instead of embedded blobs), hosted checkout and subscription billing, and — unlike v1 — the platform communicates directly with your customers by email on your behalf.
If you want to start calling the API right away, jump to the Quickstart.
How the pieces fit together¶
Everything in v2 hangs off one spine: a customer has a protocol (their regimen — the products and doses a practitioner assigned them). A protocol is turned into money and shipments through checkout (a hosted payment page) or a direct order; subscription-mode checkout creates a subscription that generates a new order every billing cycle. Refunds are issued against an order's payment.
┌──────────────┐
Products ───────▶ │ Protocol │ the customer's regimen
(catalog, read) │ (regimen) │◀──── Protocol templates / DailyStak library
└──────┬───────┘
Customer ────────────────────┤
▼
┌──────────────┐ one_time mode
│ Checkout │──────────────────────┐
│ session │ ▼
└──────┬───────┘ ┌─────────┐ ┌─────────┐
│ subscription mode │ Order │────▶ │ Refunds │
▼ └─────────┘ └─────────┘
┌──────────────┐ renewal each cycle ▲
│ Subscription │──────────────────────┘
└──────────────┘
Supporting resources:
- Products (
GET /v2/products) — the catalog you build regimens from, with your partner-specific pricing. See Products. - Documents and the read-only DailyStak library — files you attach to protocols and customers. See Documents and Templates & the DailyStak Library.
- Protocol templates — reusable regimen starting points, yours (
/v2/protocol-templates) or DailyStak-curated (/v2/library-templates). See Templates & the DailyStak Library. - Amendments and scheduled changes — the consent-gated way to edit a regimen once it has a live subscription. See Protocols & Amendments.
- Partner settings, shipping methods, custom checkout domains — account plumbing under
GET /v2/partners/settingsandGET /v2/shipping-methods. See Partner Settings & Custom Domains. - Webhooks — signed event notifications for everything above. See Webhooks.
Guide index: Quickstart · How It All Fits Together · Customers · Protocols & Amendments · Checkout · Carts · Orders · Subscriptions · Refunds · Products · Templates & the DailyStak Library · Documents · Partner Settings & Custom Domains · Webhooks · Migrating from v1
Environments¶
| Production | Sandbox | |
|---|---|---|
| API base URL | https://api.fulfillment.juniperhealth.com |
https://api.fulfillment.sandbox.juniperhealth.com |
| Token endpoint | https://auth.fulfillment.juniperhealth.com/oauth2/token |
https://auth.fulfillment.sandbox.juniperhealth.com/oauth2/token |
| Cart host (shopper cart routes; see Carts) | https://cart.fulfillment.juniperhealth.com |
https://cart.fulfillment.sandbox.juniperhealth.com |
Sandbox is a complete, isolated deployment of the platform — same endpoints, same authentication — with its own data and its own credentials. Nothing crosses between environments: a sandbox API key cannot call production, sandbox customers and orders exist only in sandbox, and no real payments are taken there. Build and test against sandbox first, then mint a separate production credential when you are ready to go live.
One caveat: payment-settlement events do not flow end to end in sandbox, so flows that complete asynchronously after a payment — hosted-checkout completion and refund status transitions — may not finish there. Test the request/response side of those flows in sandbox and verify the completion side with a low-value transaction in production.
All v2 paths already carry the /v2/ prefix, so requests go to {base URL}/v2/... — for example https://api.fulfillment.sandbox.juniperhealth.com/v2/orders.
Authentication in brief¶
v2 uses OAuth 2.0 client credentials with a dedicated v2 API key that you mint in the partner portal. Exchange your client ID and secret for a bearer token at the token endpoint; tokens last one hour.
Three scopes cover the whole API:
| Scope | Grants |
|---|---|
order/v2.read |
Read access to every v2 resource except the product catalog |
order/v2.write |
Write access (POST, PUT, PATCH, DELETE) — always required in addition to order/v2.read |
order/v2.product.read |
The product catalog: /v2/products* and /v2/ingredient-families |
Two rules worth internalizing:
- v1 and v2 keys are disjoint. A v1 API key cannot call any
/v2/*endpoint, and a v2 key cannot call/v1/*. A single credential is entirely v1 or entirely v2 — never a mix. Adopting v2 always means minting a new key. - Writes need both scopes. Every write endpoint requires the read scope and its
.writecounterpart on the token.
A missing or unparseable token returns 401; a token that lacks a required scope, is expired, or belongs to a suspended partner account returns 403. The Quickstart walks through minting a key and getting your first token.
Wire conventions¶
These hold across the entire v2 API. Each is stated once here so the per-resource guides don't repeat them.
JSON is camelCase — with one exception¶
Requests and responses use camelCase keys (firstName, shippingAddress, zipCode, createdAt). The one exception is the keys inside uploadPost.fields when you upload a document: they are raw storage form-field names (key, policy, x-amz-*), and you send them back exactly as given. See Documents.
Query parameters are camelCase too (updatedAfter, updatedBefore, customerId, includeDisabled).
Timestamps¶
ISO 8601 in UTC, e.g. 2026-07-25T14:30:12+00:00. Webhook envelopes additionally carry a Unix-seconds created field.
Nulls are present¶
Fields with no value are returned as explicit null, not omitted. Don't treat key presence as a signal; check values.
Money¶
Two representations, by design:
- Decimal strings everywhere pricing is displayed or quoted: product pricing, checkout amounts, order totals —
"29.70"means $29.70. - Integer cents on the refund API: refund requests and refund amounts use
2970for $29.70.
Never parse decimal-string amounts as floats for arithmetic; keep them as decimals.
Identifiers¶
Resource IDs are 27-character KSUIDs (e.g. 2vSGym0bH8qVEwCIGlyFoRgJq1A) — opaque, URL-safe, and sortable by creation time. The one exception: product IDs are integers (e.g. 77123129891). Webhook-related IDs carry a type prefix: evt_ (events), whep_ (endpoints), whk_ (delivery attempts).
Pagination¶
List endpoints are cursor-paginated: pass limit and the previous response's cursor, and keep paging until the cursor comes back null. Two things trip integrators up:
- A short (or empty) page does not mean you're done. Filtered lists can return fewer than
limititems — even zero — while more results remain. Only anullcursor ends the walk. - The cursor field name varies by endpoint (
cursoron most lists,nextCursoron checkout sessions; the customer list also returns ahasMoreboolean — page until it isfalse). The rule is always the same; check each operation in the API reference for the exact field.
Cursors are opaque — never construct or modify one — and cursors from search-mode responses are not interchangeable with plain-list cursors.
Errors¶
- Errors return
{"message": "...", "code": "..."}with a conventional status code, plus adetailsobject when the refusal names specifics:
{
"message": "One or more requested products were not found",
"code": "PRODUCTS_NOT_FOUND",
"details": { "missingProductIds": [1001] }
}
- Branch on
code, never onmessage. Messages are reworded without notice.detailsis omitted entirely when a refusal has no specifics — it is nevernull. - Each operation in the API reference lists the codes it can return, and which
detailskeys come with them. - Validation failures are
422withcode: VALIDATION_FAILED.details.errorslists each failure by type (missing,int_parsing, ...); field locations and rejected values are never sent back. - Two bodies differ, both by design:
- Partner-hosted (Elements) checkout refusals carry the code in
errorinstead:{"message": "...", "error": "ELEMENTS_CHECKOUT_DISABLED"}. See Checkout. - A
401from the API gateway itself (missing, malformed or expired token) carriesmessageonly. - No body ever carries both
errorandcode, so readingcodewith a fallback toerrorcovers every case.
Tenancy: everything is yours, or it's a 404¶
Every resource is scoped to your partner account. Another partner's order, customer, or session is indistinguishable from one that doesn't exist — you get a 404, never a 403. If a lookup 404s unexpectedly, first check you're using the ID in the right environment (sandbox IDs don't exist in production).
v2 emails your customers¶
This is the biggest behavioral difference from v1. On v1, you own all customer communication. On v2, the platform sends branded transactional email directly to your customers: order confirmations, checkout links, subscription lifecycle notices (created, renewal reminders, payment failures, paused, cancelled), regimen-change review requests, and refund confirmations.
Each of these has an individual toggle in your partner settings (all default on for v2 partners) — see Partner Settings & Custom Domains. Customer sign-in (magic-link) and marketing email sit behind a separate master switch that DailyStak enables when you go live on v2.
Webhooks at a glance¶
Rather than polling, subscribe to signed webhook events. Every event ships as:
{
"id": "evt_2vUjK9k3kQbH8qVEwCIGlyFoRgJ",
"type": "order.shipped",
"created": 1784988000,
"data": { "orderId": "2vTgd30JFgnwLrgb383PcV9Rken", "...": "..." }
}
signed with an HMAC in the X-Juniper-Signature header. Event families:
| Family | Events |
|---|---|
order.* |
created, updated, cancelled, shipped, delivered, exception, fulfilled |
subscription.* |
created, updated, paused, resumed, cancelled, skipped, rescheduled, order_created, renewal_pending, renewal_blocked, payment_failed |
customer.* |
created, updated, deleted |
protocol.* |
created, updated, archived |
protocol_change_request.* |
created, approved, rejected, cancelled (amendment lifecycle) |
refund.* |
created, completed |
contact.* |
received (storefront contact form) |
Delivery is at-least-once with retries — deduplicate on the event id. Signature verification, endpoint management, and retry behavior are covered in Webhooks.
Good to know¶
- Quoted price is charged price. The same pricing engine computes checkout previews, order creation, and subscription renewals, and the resulting breakdown is frozen onto the order at creation. Later catalog or discount changes never reprice an existing order, and refund ceilings read the frozen amounts.
- No idempotency keys on creates. Retrying a successful
POST /v2/ordersorPOST /v2/checkout/sessionscreates a duplicate order — never a duplicate customer; the retry resolves to the record the first call created. Use your ownsourceIdwhen retrying customer creation, and confirm outcomes (via webhook or a GET) before retrying writes. - Order state advances asynchronously. Fulfillment, shipping, and delivery updates arrive from the pharmacy and carriers; consume
order.*webhooks rather than polling for state changes. - Throttling. The API applies platform-level rate limiting. If you receive
429responses, back off exponentially; keep sustained request rates modest and use webhooks instead of tight polling loops. - Checkout URLs are credentials. The hosted checkout link embeds an access token that is the only thing protecting the payment page. Deliver it to the customer; don't log it or post it anywhere public.
- v1 documentation doesn't apply. Field shapes, authentication, and webhooks all differ. If you're migrating, treat v2 as a new integration that happens to share your partner account — see Migrating from v1.