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, andexternalalways come from the product catalog at write time — values you send for them are ignored, so a template can never misrepresent a product.packDescriptionuses 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. documentIdsandsourceTemplateIdcannot be set here. Directly-created templates always start withdocumentIds: []andsourceTemplateId: null.sourceTemplateIdis 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.
List and search¶
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
documentIdsto[]andsourceTemplateIdtonull— 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 onsourceTemplateIdto 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 behttpson 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 sendingnullor 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'ssearchmatches 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, notgut health). An unknown category returns an emptytemplatesarray, 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:
sourceTemplateIdon the copy points back at the library template. - Copies the library template's
documentIdsand 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 withGET /v2/protocol-templates/{id}— that view always carriesdocumentIds.
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}clearsdocumentIdsandsourceTemplateIdon 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 it200, 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-templatesbut 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
lineItemsto refresh its snapshots. - Dosing schedules aren't validated on templates.
administrationkeys are optional (defaulting to 0) and templates don't require the daypart sum to equalquantity. 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-templatesandGET /v2/library-templatesreturn 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 swaporder/order.readfor theorder/v2.*scopes. See Migrating from v1.