Quickstart¶
This guide takes you from zero to a paid order in the sandbox: mint a v2 API key, get a token, browse the catalog, create a customer and their protocol, send them through hosted checkout, and watch the order land.
Everything below runs against sandbox (https://api.fulfillment.sandbox.juniperhealth.com). No real payments are taken there. When you're ready for production, repeat step 1 in the production portal and swap both hostnames — see Environments.
Prerequisites: a DailyStak partner account and admin access to the partner portal.
1. Mint a v2 API key¶
v2 credentials are minted in the partner portal, on the API Credentials page (admin role required):
- Create a new credential.
- Select the v2 scopes:
order/v2.read,order/v2.write, andorder/v2.product.read. - Save the client secret immediately — it is shown once and cannot be retrieved later.
Two things to get right here:
- Explicitly pick the v2 scopes. A credential created without a scope selection is a v1 key, and v1 keys cannot call any
/v2/*endpoint (see Authentication in brief). - A key is all-v1 or all-v2. You can't mix scopes across versions on one credential; if you also use the v1 API, keep two keys.
Export the pair for the commands below:
export JUNIPER_CLIENT_ID="1a2b3c4d5e6f7g8h9i0jklmnop"
export JUNIPER_CLIENT_SECRET="…" # shown once at creation
2. Get an access token¶
Exchange the credential for a bearer token with the OAuth 2.0 client-credentials grant:
curl -s https://auth.fulfillment.sandbox.juniperhealth.com/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=$JUNIPER_CLIENT_ID" \
-d "client_secret=$JUNIPER_CLIENT_SECRET"
{
"access_token": "eyJraWQiOiJ...",
"expires_in": 3600,
"token_type": "Bearer"
}
Tokens last one hour and carry the scopes your credential was minted with. Cache the token and reuse it until shortly before expires_in elapses — don't request a fresh token per API call.
export TOKEN="eyJraWQiOiJ..."
export API="https://api.fulfillment.sandbox.juniperhealth.com"
Every API call below sends it as Authorization: Bearer $TOKEN.
3. List products¶
Browse the catalog to find product IDs for the regimen. This endpoint uses your order/v2.product.read scope:
curl -s "$API/v2/products?limit=2" \
-H "Authorization: Bearer $TOKEN"
Abridged response — note product IDs are integers, and pricing comes back as decimal strings with your partner-specific breakdown:
{
"products": [
{
"id": 77123129891,
"name": "Elite EPA",
"description": "High-potency omega-3 fish oil",
"packDescription": "Whole Body Wellness",
"external": false,
"form": "Softgel",
"status": "ACTIVE",
"pricing": {
"retailPrice": "0.53",
"partnerPrice": "0.32",
"customerPrice": "0.38",
"customerDiscount": 28,
"partnerDiscount": 40,
"commission": "0.06"
},
"recommendedUsage": { "morning": 1, "afternoon": 0, "evening": 1, "sleep": 0 },
"createdAt": "2026-01-14T17:02:56+00:00",
"updatedAt": "2026-06-30T09:12:04+00:00"
}
],
"cursor": "eyJwayI6ICJQUk9EVUNUIzc3MTIzMTI5ODkxIn0=",
"totalHits": null
}
Keep paging until cursor is null. You can also pass search=omega for fuzzy catalog search, or customerId to price for a specific customer. Details in Products.
4. Create a customer¶
Customers are your patients/clients on the platform. sourceId is your own system's ID for them — it's unique per partner and doubles as your idempotency key on retries:
curl -s -X POST "$API/v2/customers" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"sourceId": "crm-1042",
"firstName": "Jane",
"lastName": "Doe",
"email": "jane.doe@example.com",
"phone": "3125550188",
"shippingAddress": {
"line1": "123 Main St",
"city": "Austin",
"state": "TX",
"zipCode": "78701"
}
}'
{
"id": "2vTgd30JFgnwLrgb383PcV9Rken",
"partnerId": "2OKpLhTlpCrQHabz5X9qRbH4abc",
"sourceId": "crm-1042",
"firstName": "Jane",
"lastName": "Doe",
"fullName": "Jane Doe",
"email": "jane.doe@example.com",
"phone": "3125550188",
"shippingAddress": {
"line1": "123 Main St",
"line2": null,
"city": "Austin",
"state": "TX",
"zipCode": "78701"
},
"billingAddress": null,
"discountOverride": null,
"partnerConsent": false,
"createdAt": "2026-07-25T14:02:11+00:00",
"updatedAt": "2026-07-25T14:02:11+00:00"
}
A duplicate sourceId or email returns 409. This create emits the customer.created webhook event. Save the id — protocols reference it:
export CUSTOMER_ID="2vTgd30JFgnwLrgb383PcV9Rken"
5. Create a protocol¶
A protocol is the customer's regimen: which products, how many, and when in the day. It deliberately contains no shipping or billing details — those are chosen at checkout. For pack (non-external) products, administration is required and its dayparts must sum to quantity:
curl -s -X POST "$API/v2/protocols" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"customerId": "'"$CUSTOMER_ID"'",
"lineItems": [
{
"productId": 77123129891,
"quantity": 2,
"administration": { "morning": 1, "evening": 1 }
}
]
}'
Abridged response:
{
"id": "2vSGym0bH8qVEwCIGlyFoRgJq1A",
"status": "ACTIVE",
"orderType": null,
"customer": {
"id": "2vTgd30JFgnwLrgb383PcV9Rken",
"sourceId": "crm-1042",
"firstName": "Jane",
"lastName": "Doe",
"email": "jane.doe@example.com"
},
"lineItems": [
{
"productId": 77123129891,
"name": "Elite EPA",
"quantity": 2,
"administration": { "morning": 1, "afternoon": 0, "evening": 1, "sleep": 0 }
}
],
"hasExternal": false,
"createdAt": "2026-07-25T14:05:40+00:00",
"updatedAt": "2026-07-25T14:05:40+00:00"
}
A customer may hold several active protocols at once, and this emits protocol.created. See Protocols.
export PROTOCOL_ID="2vSGym0bH8qVEwCIGlyFoRgJq1A"
6. Create a checkout session¶
A checkout session is a hosted, tokenized payment page, valid for 24 hours. Created from a protocolId, it pulls the line items from the regimen and the customer's identity and shipping address from their record. mode: "subscription" means completing checkout also creates a subscription with the card saved for renewals (daysSupply is required in that mode):
curl -s -X POST "$API/v2/checkout/sessions" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"protocolId": "'"$PROTOCOL_ID"'",
"mode": "subscription",
"daysSupply": 30,
"successUrl": "https://partner.example.com/thanks",
"source": "api"
}'
Abridged response:
{
"sessionId": "2vUhN8mPzQxWvYtR5cJkLbD3eFg",
"status": "pending",
"mode": "subscription",
"customerEmail": "jane.doe@example.com",
"customerFirstName": "Jane",
"customerLastName": "Doe",
"daysSupply": 30,
"lineItems": [
{
"id": "2vUhN9aBcDeFgHiJkLmNoPqRsTu",
"productId": "77123129891",
"name": "Elite EPA",
"quantity": 2,
"unitPrice": "0.38"
}
],
"subtotal": "22.80",
"totalAmount": "24.68",
"currency": "usd",
"shippingMethod": null,
"checkoutUrl": "https://api.fulfillment.sandbox.juniperhealth.com/checkout/9f4c1e8a7b2d4c0a9e6f3b1d8c5a2e70",
"orderId": null,
"subscriptionId": null,
"expiresAt": "2026-07-26T14:09:22+00:00",
"createdAt": "2026-07-25T14:09:22+00:00",
"updatedAt": "2026-07-25T14:09:22+00:00"
}
Send your customer to checkoutUrl (email, SMS, or a redirect from your app). The quoted amounts already include tax and all discounts, but not shipping — totalAmount updates when the customer picks a shipping method on the page, and the charge equals that final amount. Treat the URL as sensitive: the embedded token is the only credential for the payment page.
Note productId is a string on checkout line items, unlike the integer catalog ID. Also, creating a new session for the same protocol cancels any older pending ones — at most one session per protocol is active. See Checkout.
7. Watch for completion¶
Checkout completes asynchronously: after the customer pays on the hosted page, the order (and the subscription, in subscription mode) are created within seconds. You have two ways to observe it.
Sandbox note: payment-settlement events do not flow end to end in sandbox, so this completion step may not fire there. Everything up to and including creating the session works in sandbox; verify completion with a low-value transaction in production.
Preferred — the order.created webhook. If you've configured a webhook endpoint (see Webhooks), you'll receive:
{
"id": "evt_2vUiQ3rTsUvWxYzA1bC2dE3fG4h",
"type": "order.created",
"created": 1784991012,
"data": {
"orderId": "2vUiQ2kLmNpQrStUvWxYzAbCdEf",
"status": "NEW",
"fulfillmentInfo": null,
"createdAt": "2026-07-25T14:50:12+00:00",
"updatedAt": "2026-07-25T14:50:12+00:00",
"checkoutSessionId": "2vUhN8mPzQxWvYtR5cJkLbD3eFg",
"metadata": null
}
}
checkoutSessionId is the session you created in step 6, so you can match the order to it without polling; metadata echoes whatever you sent in the session's metadata (none here).
Alternative — poll the session:
curl -s "$API/v2/checkout/sessions/2vUhN8mPzQxWvYtR5cJkLbD3eFg" \
-H "Authorization: Bearer $TOKEN"
On completion, status becomes "completed" and orderId (plus subscriptionId in subscription mode) populate. Poll at a gentle interval (a few seconds); don't hammer the endpoint.
One quirk to know: checkout-created subscriptions do not emit subscription.created — the order.created event is your completion signal for both.
8. Fetch the order¶
curl -s "$API/v2/orders/2vUiQ2kLmNpQrStUvWxYzAbCdEf" \
-H "Authorization: Bearer $TOKEN"
Abridged response:
{
"id": "2vUiQ2kLmNpQrStUvWxYzAbCdEf",
"customerId": "2vUiP9hJkLmNoPqRsTuVwXyZaBc",
"status": "NEW",
"orderType": "SUBSCRIPTION",
"daysSupply": 30,
"isFirstOrder": true,
"shippingMethod": "STANDARD",
"customer": {
"sourceId": "crm-1042",
"firstName": "Jane",
"lastName": "Doe",
"email": "jane.doe@example.com",
"address": { "line1": "123 Main St", "line2": null, "city": "Austin", "state": "TX", "zipCode": "78701" }
},
"customerTotal": "24.68",
"partnerCommission": "3.60",
"fulfillmentInfo": null,
"createdAt": "2026-07-25T14:50:12+00:00",
"updatedAt": "2026-07-25T14:50:12+00:00"
}
The pricing on the order is frozen at creation — later catalog changes never reprice it. From here the order advances through fulfillment on its own (NEW → PROCESSING → DISPENSING → FILLED → SHIPPED → DELIVERED), and each transition emits an order.* webhook event with tracking details in fulfillmentInfo once shipped. See Orders for the full lifecycle.
Where to go next¶
- Get webhooks set up properly — signature verification, retries, and the full event catalog: Webhooks
- Recurring billing — pause/resume/cancel, renewal notices, and partner-managed renewals: Subscriptions
- Changing a live regimen — the amendment flow and customer consent: Protocols & Amendments
- Refunds — reading an order's refunds and following them to completion (heads-up: amounts are integer cents): Refunds
- Your account — branding, email toggles, shipping methods, custom checkout domains: Partner Settings & Custom Domains