Skip to content

Migrating from v1

The v2 API is a new surface over the same platform: your partner account, orders, protocols, templates, and webhook endpoints all carry over. Records are shared between versions — an order or protocol template created over v1 is visible over v2, and vice versa. What changes is the credential you call with, the base path, the wire casing, and a handful of behaviors described below.

A typical migration is four steps:

  1. Mint a v2 API key in the partner portal (your v1 key cannot call v2).
  2. Swap /v1 for /v2 in your request paths, using the endpoint map below.
  3. Move your payloads to camelCase (two exceptions, listed below).
  4. Review the behavior changes — most importantly, on v2 the platform emails your customers directly unless you turn that off.

Test against sandbox (https://api.fulfillment.sandbox.juniperhealth.com) before switching production traffic.

Step 1: Mint a v2 credential

v1 and v2 keys are not interchangeable. Their OAuth scopes are disjoint by design:

v1 scopes v2 scopes
Read order/order.read order/v2.read
Write order/order.write order/v2.write
Catalog order/product.read order/v2.product.read

A v1 token calling any /v2/* endpoint gets 403 {"message": "Forbidden"}, and a v2 token gets the same on /v1/*. There is no way to add v2 scopes to an existing v1 key — a credential is all-v1 or all-v2.

Create the new key in the partner portal (API Credentials, admin users only) and explicitly select the v2 scopes — a credential created without a scope selection defaults to v1. Write endpoints require both order/v2.read and order/v2.write on the token, so request both unless the integration is read-only.

Token exchange is unchanged — same endpoints, same client-credentials flow, 1-hour tokens:

curl -s https://auth.fulfillment.juniperhealth.com/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials&client_id=YOUR_V2_CLIENT_ID&client_secret=YOUR_V2_CLIENT_SECRET"

Keep your v1 key active while you migrate; the two can run side by side against the same account, and you can revoke the v1 key once cutover is complete.

Step 2: Swap the base path

v1 v2 Notes
GET/POST /v1/orders GET/POST /v2/orders Create now emails your customer — see below
GET /v1/orders/{order_id} GET /v2/orders/{orderId}
PUT /v1/orders/{order_id} — (removed) v2 has no full replace; only the shipping address can change after creation — otherwise cancel and re-create
DELETE /v1/orders/{order_id} — (removed) Use POST /v2/orders/{orderId}/cancel
PATCH /v1/orders/{order_id}/address PATCH /v2/orders/{orderId}/address Request body is camelCase (zipCode)
POST /v1/orders/{order_id}/cancel POST /v2/orders/{orderId}/cancel Same rule on both: partner-managed orders, NEW or PROCESSING until dispensing; never refunds
GET /v1/orders/{order_id}/refunds (+ /{refund_id}) GET /v2/orders/{orderId}/refunds (+ /{refundId}) Read-only on both; refunds are issued by DailyStak — see below
GET/POST /v1/protocols GET/POST /v2/protocols
GET/PUT/DELETE /v1/protocols/{protocol_id} GET/PUT/DELETE /v2/protocols/{protocolId}
/v1/protocol-templates (+ /{template_id}) /v2/protocol-templates (+ /{templateId}) Payloads are byte-identical by design
GET /v1/products (+ /{product_id}) GET /v2/products (+ /{productId}) Requires order/v2.product.read
GET /v1/shipping-methods GET /v2/shipping-methods EXPRESS renamed EXPEDITED — see below
GET/PATCH /v1/partners/settings GET/PATCH /v2/partners/settings
/v1/partners/settings/webhook-secret, .../rotate-webhook-secret /v2/partners/settings/webhook-secret, .../rotate-webhook-secret Legacy single-URL webhook secret
/v1/partners/settings/custom-domain (+ /verify) /v2/partners/settings/custom-domain (+ /verify)
/v1/partners/webhooks (+ /{endpoint_id}) — (portal) Webhook endpoints are managed in the partner portal; see Webhooks

v2 also adds resources v1 never had — customers, subscriptions, hosted checkout, documents, amendments, and scheduled changes. Those are new integrations rather than migrations; start with the API reference and the sibling guides.

Step 3: Move to camelCase

v1 payloads were documented in snake_case. On v2, requests and responses are camelCase: line_items becomes lineItems, days_supply becomes daysSupply, shipping_method becomes shippingMethod, source_id becomes sourceId, zip_code becomes zipCode, and so on. A v2 order create looks like:

curl -s -X POST https://api.fulfillment.juniperhealth.com/v2/orders \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "customer": {
      "sourceId": "cust-1029",
      "firstName": "Jane",
      "lastName": "Doe",
      "email": "jane.doe@example.com",
      "phone": "(555) 010-2030",
      "address": {"line1": "437 Lytton Ave", "line2": null, "city": "Palo Alto", "state": "CA", "zipCode": "94301"}
    },
    "daysSupply": 30,
    "orderType": "ONE_TIME",
    "shippingMethod": "STANDARD",
    "lineItems": [
      {
        "productId": 77123129891,
        "quantity": 2,
        "administration": {"morning": 1, "afternoon": 0, "evening": 1, "sleep": 0},
        "note": "Take with breakfast and dinner",
        "takeWith": "food"
      }
    ]
  }'

The 201 response is camelCase as well (abbreviated):

{
  "id": "2vSGym0bH8qVEwCIGlyFoRgJq1A",
  "customerId": "2vSGxTzqD4rK9mLnPwYvAbCdEfG",
  "status": "NEW",
  "isFirstOrder": true,
  "daysSupply": 30,
  "orderType": "ONE_TIME",
  "shippingMethod": "STANDARD",
  "customer": {
    "sourceId": "cust-1029",
    "firstName": "Jane",
    "lastName": "Doe",
    "email": "jane.doe@example.com",
    "phone": "5550102030",
    "address": {"line1": "437 Lytton Ave", "line2": null, "city": "Palo Alto", "state": "CA", "zipCode": "94301"}
  },
  "customerTotal": "33.39",
  "partnerCommission": "5.40",
  "createdAt": "2026-07-25T12:00:00+00:00",
  "updatedAt": "2026-07-25T12:00:00+00:00"
}

Formerly snake_case spots

Two places on the migrated surface used to keep v1's snake_case for byte-for-byte parity. Both are camelCase now, like the rest of v2:

  1. PATCH /v2/orders/{orderId}/address — send zipCode.
{"line1": "123 Updated St", "line2": "Apt 4", "city": "Springfield", "state": "IL", "zipCode": "62704"}
  1. /v2/protocol-templates — the DELETE success body is keyed templateId, and the product-validation 400s carry details.missingProductIds / details.productsNotAvailable.

Behavior changes to plan for

The platform now emails your customers

This is the most important behavioral change in v2. On v1, DailyStak never contacted your customers — you owned all communication. Orders created through v2 (and the v2 checkout and subscription features) trigger customer-facing emails sent by the platform, branded with your logo and colors.

The per-event toggles are all on by default: order confirmations (sent when you POST /v2/orders with a customer email on the order), checkout links, subscription created/renewal/payment/paused/cancelled notices, amendment (change-request) notices, and refund confirmations. If you already send your own transactional email and don't want customers hearing from the platform, turn the relevant toggles off in the partner portal's notification settings before you send production traffic to v2 — otherwise your customers get two emails per event, starting with your very first v2 order.

Customer sign-in links and marketing sends are separately gated behind a master switch that stays off until DailyStak enables it with you as part of going live on v2.

EXPRESS was renamed EXPEDITED

The middle shipping tier is now EXPEDITED; the enum is STANDARD | EXPEDITED | OVERNIGHT. Update any hardcoded EXPRESS values — v2 also validates that shippingMethod is one of the methods configured for your account, and rejects anything else with:

{
  "message": "Invalid shipping method",
  "code": "INVALID_SHIPPING_METHOD",
  "details": { "availableShippingMethods": ["STANDARD", "EXPEDITED"] }
}

Fetch your configured methods (names and prices) from GET /v2/shipping-methods.

Refunds are read-only

Neither v1 nor v2 has a refund-creation endpoint any more: refunds are issued by DailyStak, not through the API. You can still list an order's refunds (GET /v2/orders/{orderId}/refunds) and fetch one (GET /v2/orders/{orderId}/refunds/{refundId}); responses are camelCase and amounts are integer cents. Completion arrives asynchronously via the refund.completed webhook event. See Refunds.

Orders can no longer be replaced

v1 allowed PUT (full replace) and DELETE on an order. On v2, an order is priced and frozen at creation; after that you can only update its shipping address (PATCH /v2/orders/{orderId}/address, while the order is still in NEW or PROCESSING) or cancel it (POST /v2/orders/{orderId}/cancel, same statuses). To change anything else, cancel and create a new order. See Orders.

What carries over unchanged

  • Webhooks. Your registered endpoints, secrets, event types, signing (X-Juniper-Signature), and delivery behavior are identical on v2 — events fire regardless of which API version created the resource, and nothing needs re-registering. One split to note: the endpoint-management API lives under /v1/partners/webhooks, which a v2 key cannot call, so on v2 you manage endpoints through the partner portal. The legacy single webhookUrl (set via PATCH /v2/partners/settings) and its secret endpoints remain available on v2. Details in Webhooks.
  • Protocol templates. Request and response payloads are byte-identical to v1 by design, and both versions operate on the same stored templates — only the base path and credential change. See Protocol Templates.
  • Your data. Orders, protocols, and settings are shared across versions. There is no backfill, export, or re-import step.
  • Environments and token flow. Same production and sandbox hosts, same OAuth token endpoints, same 1-hour token lifetime.

Good to know

  • Run both keys in parallel during migration — v1 keeps working while you move traffic endpoint by endpoint. Turn off the customer-email toggles before the first production v2 order if you don't want the platform emailing customers.
  • A 403 on v2 usually means a v1 key. If a request that works on /v1 returns 403 {"message": "Forbidden"} on /v2, check which credential minted the token and that it carries the order/v2.* scopes (writes need order/v2.read and order/v2.write).
  • Product endpoints use a separate scope. GET /v2/products and GET /v2/ingredient-families require order/v2.product.read; include it when minting the key if you read the catalog.
  • snake_case request keys are tolerated, not supported. Most v2 endpoints happen to accept snake_case input today; this is not part of the contract. Send camelCase everywhere.
  • List pagination is cursor-based. GET /v2/orders returns {"orders": [...], "cursor": "..."}; pass the cursor back verbatim and stop when it is null. Cursors are opaque — never construct or modify them.
  • Response conventions. Null fields are present (not omitted), timestamps are ISO 8601 UTC, order money fields are 2-decimal strings (e.g. "29.70"), and refund amounts are integer cents.
  • No idempotency keys. Neither order create nor refund create accepts one; a retried request after a successful response creates a duplicate. Treat 201 as committed and reconcile via GET before retrying.