Skip to content

Templates & the DailyStak Library

A protocol template is a reusable, named group of product line items — quantities, optional dosing schedules, notes — that you apply when building protocols for your customers. Templates are building blocks, not orders: creating, editing, or deleting a template never touches an existing protocol, order, or subscription, and has no billing effect.

There are two template surfaces in v2, and it pays to keep them straight:

/v2/library-templates /v2/protocol-templates
Owned by DailyStak (curated, shared across all partners) You
Operations List, get, copy — read-only catalog Full CRUD
category field Yes No
sourceTemplateId Not applicable (these are the source) Set on templates copied from the library; null otherwise

Copying a library template creates a template you own. From that moment it appears in GET /v2/protocol-templates alongside templates you authored yourself, and you manage it exactly the same way.

The DailyStak library also includes library documents — research PDFs and similar reference material you can browse and download. They live at /v2/library-documents and are read-only too; when a library template carries document attachments, they travel with any copy you make of it.

Authentication

All endpoints use OAuth2 client credentials with a v2 API key minted in the partner portal. v1 keys cannot call these routes (and v2 keys cannot call v1). Reads require order/v2.read; writes require both order/v2.read and order/v2.write on the token:

Operation Scopes
GET /v2/protocol-templates, GET /v2/protocol-templates/{templateId} order/v2.read
POST /v2/protocol-templates, PUT /v2/protocol-templates/{templateId}, DELETE /v2/protocol-templates/{templateId} order/v2.read + order/v2.write
GET /v2/library-templates, GET /v2/library-templates/{templateId} order/v2.read
POST /v2/library-templates/{templateId}/copy order/v2.read + order/v2.write
GET /v2/library-documents (list, get, download) order/v2.read

Your templates are strictly partner-scoped: you can never read or modify another partner's templates, and requesting someone else's template ID returns a plain 404 — indistinguishable from an ID that doesn't exist.

Your protocol templates

Create a template

POST /v2/protocol-templates creates a template with 1–99 line items. Every productId must reference an existing, orderable product from the catalog — unknown or discontinued/draft products reject the whole request with 400.

curl -X POST https://api.fulfillment.juniperhealth.com/v2/protocol-templates \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Daily Wellness Protocol",
    "description": "A daily wellness protocol for gut health",
    "lineItems": [
      {
        "productId": 1001,
        "quantity": 2,
        "administration": { "morning": 1, "afternoon": 0, "evening": 1, "sleep": 0 },
        "note": "Take with food"
      }
    ]
  }'

Response 201 Created:

{
  "id": "2u5nKcXhYwZAbCdEfGhIjKlMnOp",
  "name": "Daily Wellness Protocol",
  "description": "A daily wellness protocol for gut health",
  "documentIds": [],
  "sourceTemplateId": null,
  "image": null,
  "tags": [],
  "lineItems": [
    {
      "productId": 1001,
      "quantity": 2,
      "name": "Probiotic",
      "image": "https://cdn.example.com/probiotic.jpg",
      "administration": { "morning": 1, "afternoon": 0, "evening": 1, "sleep": 0 },
      "packDescription": "30 capsules",
      "external": false,
      "note": "Take with food"
    }
  ],
  "createdAt": "2026-03-09T12:00:00Z",
  "updatedAt": "2026-03-09T12:00:00Z"
}

Two things to notice:

  • Catalog enrichment. Line-item name, image, and external always come from the product catalog at write time — values you send for them are ignored, so a template can never misrepresent a product. packDescription uses your value if you supply one, otherwise the product's own pack description. These are snapshots: a later product rename does not rewrite stored templates.
  • documentIds and sourceTemplateId cannot be set here. Directly-created templates always start with documentIds: [] and sourceTemplateId: null. sourceTemplateId is only ever set by the library copy flow, and document attachments are managed through the Documents assignment endpoints.

There is no idempotency key on this endpoint. Retrying a create whose response you never saw can produce a duplicate template; list your templates and de-duplicate by name if you're unsure whether a create landed.

GET /v2/protocol-templates returns all of your active templates in one response — no pagination — in creation order. The optional search parameter is a case-insensitive substring match against the template name, any line item's product name, or any of the template's tags:

curl "https://api.fulfillment.juniperhealth.com/v2/protocol-templates?search=probiotic" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
{ "templates": [ { "id": "2u5nKcXhYwZAbCdEfGhIjKlMnOp", "name": "Daily Wellness Protocol", "...": "..." } ] }

An empty or non-matching search just returns {"templates": []} — never a 404.

Get one template

GET /v2/protocol-templates/{templateId} returns the same shape as create. A missing ID (or an ID owned by another partner) returns 404.

Update a template

PUT /v2/protocol-templates/{templateId} uses partial-update semantics despite the verb. All body fields are optional; an omitted or explicitly null field keeps its current value — which means a description can be changed but never cleared back to null through this endpoint. When you do supply lineItems, the array replaces the entire set (no per-item merging) and goes through the same catalog validation and enrichment as create.

curl -X PUT https://api.fulfillment.juniperhealth.com/v2/protocol-templates/2u5nKcXhYwZAbCdEfGhIjKlMnOp \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Renamed Protocol" }'

Warning: every update resets documentIds to [] and sourceTemplateId to null — regardless of what you send. Updating a template you copied from the DailyStak library severs its library lineage and drops its document attachments from the template record. If you rely on sourceTemplateId to track which library template a copy came from, record it on your side before you ever update the copy, and re-create document assignments afterwards via the Documents endpoints.

There is no optimistic concurrency control on updates: no ETag, no If-Match, last write wins. If two systems edit the same template concurrently, one edit silently overwrites the other.

Image and tags

Two optional fields describe a template for display and search. Both are accepted on create and update, and both are always present in the response: image is null and tags is [] until you set them.

  • image — the URL of a file from your DailyStak media library, at most 2048 characters. Only the URL is checked: it must be https on the DailyStak media host, with no credentials, whitespace or control characters. Whether the file exists, or who uploaded it, is not checked. On create, omitting it (or sending null or a blank string) means no image.
  • tags — free-text labels, normalized on write: each is trimmed, has inner whitespace collapsed and is lower-cased, then blanks and duplicates are dropped (the first occurrence wins). After normalization each tag may be at most 30 characters and at most 10 may remain; a request may carry up to 50 raw entries. The list endpoint's search matches tags too.

On update, both fields follow the usual keep rule, with a way to clear each:

You send image tags
Nothing, or null Kept Kept
"" / [] Cleared to null Cleared to []
A value Replaced Replaced wholesale — no merging
curl -X PUT https://api.fulfillment.juniperhealth.com/v2/protocol-templates/2u5nKcXhYwZAbCdEfGhIjKlMnOp \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "image": "", "tags": [" Gut Health", "Sleep", "gut health"] }'

The response carries "image": null and "tags": ["gut health", "sleep"].

An image or tag that fails these checks is refused with 422 and code: INVALID_TEMPLATE_FIELD, and nothing is written. An image over 2048 characters, more than 50 tags, or a value of the wrong type gets 422 with code: VALIDATION_FAILED instead. A template copied from the library starts with image: null and tags: [].

Delete a template

DELETE /v2/protocol-templates/{templateId} is a soft delete: the template drops out of GET /v2/protocol-templates immediately and permanently — there is no undelete. Deleting a template has no effect on protocols previously built from it.

The success response is 200 (not 204):

{
  "templateId": "2u5nKcXhYwZAbCdEfGhIjKlMnOp",
  "message": "Protocol template deleted successfully"
}

Repeating the delete returns 200 again, so retries are safe. A deleted template remains readable by ID (see Good to know); treat the list endpoint as your source of truth for which templates exist.

The DailyStak library

Browse library templates

GET /v2/library-templates returns the full active catalog in one response (no pagination). Two filters, combinable with AND:

  • category — exact, case-sensitive match (Gut Health, not gut health). An unknown category returns an empty templates array, not an error. Category-filtered results come back sorted by name.
  • search — case-insensitive substring match over template names and line-item product names.
curl "https://api.fulfillment.juniperhealth.com/v2/library-templates?category=Gut%20Health&search=probiotic" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Library templates look like your own templates plus a category field; their documentIds array points at library documents. Their line items carry the same catalog snapshots (name, image, packDescription, external) taken when DailyStak authored the template. GET /v2/library-templates/{templateId} fetches one by ID.

Copy a library template

POST /v2/library-templates/{templateId}/copy — no request body — clones a library template into your account:

curl -X POST https://api.fulfillment.juniperhealth.com/v2/library-templates/2VJq6mF9y0kZbPq0d3XyYw1AbCd/copy \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Response 201 Created:

{
  "id": "2VJrTk3sN8wQhXyZaB1cD2eF3gH",
  "name": "Gut Health Protocol",
  "description": "A template for gut health",
  "sourceTemplateId": "2VJq6mF9y0kZbPq0d3XyYw1AbCd",
  "image": null,
  "tags": [],
  "lineItems": [
    {
      "productId": 1001,
      "quantity": 2,
      "name": "Probiotic Complex",
      "image": "https://cdn.example/products/1001.png",
      "administration": { "morning": 1, "afternoon": 0, "evening": 1, "sleep": 0 },
      "packDescription": "30-day supply",
      "external": false,
      "note": null
    }
  ],
  "createdAt": "2026-07-25T16:00:00Z",
  "updatedAt": "2026-07-25T16:00:00Z"
}

What the copy does:

  • Creates a new, partner-owned template with a fresh id (note it differs from the library template's ID in the path). Name, description, and line-item snapshots are copied verbatim.
  • Records lineage: sourceTemplateId on the copy points back at the library template.
  • Copies the library template's documentIds and clones its document assignments (including their delivery modes) to your account — the attached research documents travel with the copy and become visible through your document-assignment endpoints.
  • The copy response itself does not include documentIds. To see them, fetch the new template with GET /v2/protocol-templates/{id} — that view always carries documentIds.

From here on the copy is yours: it appears in GET /v2/protocol-templates, and you update or delete it like any other template. It is a snapshot — later DailyStak edits to the library template do not propagate to your copy, and deleting your copy never affects the library.

The copy endpoint is not idempotent: every call creates another independent copy (duplicate names are allowed). If you need to guard against double-copies, check GET /v2/protocol-templates for an existing template with the matching sourceTemplateId before copying. If a copy call fails with a 5xx mid-flight, the safest remediation is to delete the resulting partial template (if one appears in your list) and copy again.

Library documents

GET /v2/library-documents lists DailyStak-provided documents — currently research PDFs, each usually tied to a catalog product. The plain listing is cursor-paginated:

curl "https://api.fulfillment.juniperhealth.com/v2/library-documents?limit=50" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
{
  "documents": [
    {
      "id": "0b0e8b9e-6d0f-4a3e-9f0a-1c2d3e4f5a6b",
      "name": "Magnesium Glycinate — Research",
      "description": "Research document for Magnesium Glycinate",
      "category": "Research",
      "fileType": "application/pdf",
      "fileSizeBytes": 482113,
      "publicUrl": "https://cdn.fulfillment.juniperhealth.com/library/0b0e8b9e-6d0f-4a3e-9f0a-1c2d3e4f5a6b/magnesium-research.pdf",
      "productId": 1001,
      "productName": "Magnesium Glycinate",
      "status": "ACTIVE",
      "createdAt": "2026-05-02T18:21:07.412331+00:00",
      "updatedAt": "2026-05-02T18:21:07.412331+00:00"
    }
  ],
  "cursor": "eyJwayI6...",
  "hasMore": true
}

Pass the returned cursor verbatim to fetch the next page; hasMore is false and cursor is null on the last page. limit accepts an integer 1–100 (default 50; out-of-range values fall back to the default). The cursor is opaque — never construct or modify one.

Adding ?productId=1001 switches to product-filter mode: all documents associated with that product are returned in a single unpaginated response (limit and cursor are ignored; cursor is always null). A non-integer productId returns 400.

GET /v2/library-documents/{documentId} fetches one document's metadata, and GET /v2/library-documents/{documentId}/download returns a download URL:

{ "downloadUrl": "https://cdn.fulfillment.juniperhealth.com/library/0b0e8b9e-6d0f-4a3e-9f0a-1c2d3e4f5a6b/magnesium-research.pdf" }

For library documents this is usually a permanent CDN URL (the same value as the document's publicUrl); when no CDN URL exists, a presigned URL valid for one hour is returned instead. Treat any download URL as valid for at least an hour and fetch a fresh one rather than persisting it long-term.

Templates and protocols

Templates never attach to a protocol as an object — you expand a template into line items when creating a protocol, and optionally record where each line came from. The line-item input on POST /v2/protocols accepts an optional templateId field ("protocol-template provenance"): set it to the template's ID on each line item you sourced from that template, and it is echoed back as templateId on the protocol's line items thereafter. This is how the portal (and your integration, if you choose) can show "this line came from the Daily Wellness template."

Provenance is informational only — the protocol keeps its own copy of every line item, so editing or deleting the template later never changes the protocol. See Protocols & Amendments for the full protocol flow.

Errors

Bodies are {"message", "code", "details"?} — branch on code — except the gateway's own 401/403, which carry no code.

Status When code
400 A line item references a product that doesn't exist PRODUCTS_NOT_FOUND; details.missingProductIds lists them
400 A line item references a discontinued or draft product PRODUCTS_NOT_AVAILABLE; details.productsNotAvailable lists them
401 Missing or unparseable token — (gateway) or UNAUTHORIZED
403 Token lacks a required scope, is expired, or is a v1 credential — (gateway)
404 Template/document not found, or not yours PROTOCOL_TEMPLATE_NOT_FOUND, LIBRARY_TEMPLATE_NOT_FOUND or DOCUMENT_NOT_FOUND
422 image isn't an https URL on the DailyStak media host, or contains credentials, whitespace or control characters; a tag is over 30 characters once normalized; more than 10 tags remain after de-duplication INVALID_TEMPLATE_FIELD
422 Request-shape validation: missing name, empty lineItems, more than 99 items, name over 100 chars, description/note over 500 chars, image over 2048 chars, more than 50 tags, malformed JSON VALIDATION_FAILED; details.errors lists each failure by type

None of the endpoints in this guide emit webhook events — template and library operations are silent.

Good to know

  • PUT resets provenance. Worth repeating: any PUT /v2/protocol-templates/{templateId} clears documentIds and sourceTemplateId on the template, no matter what the body contains. Capture lineage on your side before editing copies.
  • Soft-deleted templates stay readable by ID. After a delete, the template disappears from the list but GET /v2/protocol-templates/{templateId} still returns it 200, with no field indicating deletion. Use the list as your inventory of live templates; don't infer existence from a successful GET by ID.
  • The library list is the catalog of record. A library template retired by DailyStak drops out of GET /v2/library-templates but remains fetchable — and even copyable — by ID. If a template isn't in the list, treat it as retired.
  • Everything is a snapshot. Product data on line items (both your templates and library templates) is captured at write time; catalog changes don't flow back into stored templates. Re-save a template's lineItems to refresh its snapshots.
  • Dosing schedules aren't validated on templates. administration keys are optional (defaulting to 0) and templates don't require the daypart sum to equal quantity. That rule is enforced when line items land on a protocol — validate before you build the protocol, not when you author the template.
  • No pagination on template lists. Both GET /v2/protocol-templates and GET /v2/library-templates return the full set in one response. Only the library-documents list paginates.
  • Reads are eventually consistent. A just-created template is normally visible immediately, but strict read-after-write is not guaranteed — build retries into any create-then-fetch sequence.
  • Migrating from v1? These endpoints are wire-compatible with /v1/protocol-templates — same request and response payloads — and operate on the same underlying templates, so anything you created over v1 is already visible here. Only the base path and credential change: mint a v2 key and swap order/order.read for the order/v2.* scopes. See Migrating from v1.