Documents¶
Documents are files you upload — patient handouts, welcome packets, product one-pagers — that DailyStak delivers alongside your orders. You upload a file once, then assign it to a scope (your whole account, a single protocol, or a protocol template) with a delivery mode that controls whether a printed copy goes in the box.
Documents are distinct from library documents, DailyStak's curated read-only reference material at /v2/library-documents. Those are covered in Templates & the DailyStak Library; this guide covers the documents you own.
Authentication¶
All document endpoints use OAuth2 client credentials with a v2 API key minted in the partner portal. v1 keys cannot call v2 routes (and vice versa). Reads require order/v2.read; writes require both order/v2.read and order/v2.write on the token.
Everything is strictly partner-scoped: another partner's document or assignment ID returns a plain 404, indistinguishable from an ID that doesn't exist.
The upload lifecycle¶
Getting a file live is a three-step flow:
POST /v2/documents -> document record created (status PENDING),
upload credentials returned
Upload the file (form POST) -> file is malware-scanned asynchronously
POST /v2/documents/{documentId}/confirm -> non-PDF: ACTIVE immediately
PDF: PROCESSING, then ACTIVE (seconds later)
Only ACTIVE documents can be assigned or delivered.
Step 1 — Create the document¶
POST /v2/documents registers the metadata and returns upload credentials. No file moves in this call.
curl -X POST https://api.fulfillment.juniperhealth.com/v2/documents \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Welcome Packet",
"description": "Given with first order",
"fileType": "application/pdf",
"originalFilename": "welcome.pdf"
}'
Response 201 Created:
{
"document": {
"id": "2vXk9qAbCdEfGhIjKlMnOpQrStu",
"partnerId": "acme-clinic",
"name": "Welcome Packet",
"description": "Given with first order",
"fileType": "application/pdf",
"fileSizeBytes": null,
"publicUrl": null,
"productId": null,
"productName": null,
"originalFilename": "welcome.pdf",
"status": "PENDING",
"createdAt": "2026-07-25T17:02:11.481932Z",
"updatedAt": "2026-07-25T17:02:11.481932Z"
},
"uploadUrl": "https://uploads-bucket.s3.us-east-2.amazonaws.com/documents/...",
"uploadPost": {
"url": "https://uploads-bucket.s3.us-east-2.amazonaws.com/",
"fields": {
"Content-Type": "application/pdf",
"key": "documents/documents/acme-clinic/2vXk9qAbCdEfGhIjKlMnOpQrStu/welcome.pdf",
"policy": "eyJ...",
"x-amz-algorithm": "AWS4-HMAC-SHA256",
"x-amz-credential": "AKIA.../20260725/us-east-2/s3/aws4_request",
"x-amz-date": "20260725T170211Z",
"x-amz-signature": "abc123..."
}
},
"expiresIn": 900
}
Rules that apply at creation time:
- File-type allowlist.
fileTypemust be one of exactly five MIME types:application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document(DOCX),image/png,image/jpeg, orimage/webp. Anything else is a400. - 50-document cap. Each partner may hold at most 50 documents — all statuses count, including abandoned
PENDINGrecords. At the cap, create returns409. Free slots withDELETE /v2/documents/{documentId}. - Not idempotent. Every call mints a new document that counts toward the cap. Don't blindly retry a create that may have succeeded.
productId(an integer) andproductNameare optional associations;productIdis not validated against the catalog.
Step 2 — Upload the file¶
Use uploadPost (a presigned form POST). Send multipart/form-data to uploadPost.url, including every key from uploadPost.fields verbatim, with the file itself as the last part under the key file:
curl -X POST "https://uploads-bucket.s3.us-east-2.amazonaws.com/" \
-F "Content-Type=application/pdf" \
-F "key=documents/documents/acme-clinic/2vXk9qAbCdEfGhIjKlMnOpQrStu/welcome.pdf" \
-F "policy=eyJ..." \
-F "x-amz-algorithm=AWS4-HMAC-SHA256" \
-F "x-amz-credential=AKIA.../20260725/us-east-2/s3/aws4_request" \
-F "x-amz-date=20260725T170211Z" \
-F "x-amz-signature=abc123..." \
-F "file=@welcome.pdf"
A successful upload returns 204 with an empty body.
- Iterate
fieldsprogrammatically. The keys are raw storage form-field names (key,policy,x-amz-*, ...) — they are not camelCased, the exact set can vary, and every one must be submitted unchanged. - 15-minute window. Both upload targets expire after
expiresIn= 900 seconds. If you miss the window, the document staysPENDING; delete it and create a new one. - Size limits. The signed policy enforces 1 byte – 10 MB and pins the
Content-Typeto thefileTypeyou declared. An out-of-range file is rejected at upload time. uploadUrlis deprecated. It is a bare presigned PUT kept for backward compatibility and slated for removal. New integrations must useuploadPost.
After upload, the file is malware-scanned asynchronously before it becomes confirmable — usually within seconds, occasionally up to a minute.
Step 3 — Confirm¶
POST /v2/documents/{documentId}/confirm (no request body) tells the service the upload is done. It verifies the scanned file is in place and returns the updated document:
- Non-PDF files:
statusgoes straight toACTIVEandfileSizeBytesis populated. Done. - PDFs:
statusbecomesPROCESSINGwhile every page is stamped with a small QR code (used to verify the right document ships with the right order). Stamping typically takes seconds; pollGET /v2/documents/{documentId}untilstatusisACTIVE.
Confirm has two semantics you must handle:
404means "not yet" — retry. Because scanning is asynchronous, confirming immediately after upload can return404even though the upload succeeded. Retry with backoff. A file that never becomes confirmable (still404after a few minutes) failed the malware scan and will not be delivered; delete the record and re-upload a clean file, or contact support.- Confirm is not idempotent. Only a
PENDINGdocument can be confirmed. A repeat confirm — or a confirm during/after processing — returns400(the message readsDocument {id} is not active).
There are no webhook events for the document lifecycle — poll GET /v2/documents/{documentId} to observe PROCESSING → ACTIVE. See Webhooks for what the API does emit.
Document statuses¶
| Status | Meaning |
|---|---|
PENDING |
Created; upload not yet confirmed. Counts toward the 50-document cap. |
PROCESSING |
PDF confirmed; being stamped. Becomes ACTIVE automatically, typically in seconds. |
ACTIVE |
Live. Can be assigned, delivered, and printed. |
ARCHIVED |
Reserved — not currently set by the API. |
Reading and managing documents¶
List and get¶
GET /v2/documents returns your documents — all statuses included — in creation order, paginated:
{
"documents": [ { "id": "2vXk9qAbCdEfGhIjKlMnOpQrStu", "...": "..." } ],
"cursor": "eyJwayI6...",
"hasMore": true
}
limitis 1–100 (default 50). Out-of-range values are silently reset to 50 rather than rejected.cursoris opaque — pass it back verbatim; never parse it. An invalid cursor is silently ignored and the listing restarts from the first page.- Treat
cursor: nullas the terminator, not a short page. A page can reporthasMore: trueand the next fetch returnsdocuments: []withcursor: null. Loop untilcursorisnull.
GET /v2/documents/{documentId} returns a single document, or 404.
Update metadata¶
PATCH /v2/documents/{documentId} updates name and/or description only (any status). Omitted fields are untouched; an empty body returns 400.
description: nullclears the description.- Never send
name: null—nameis not nullable. Omit it to leave it unchanged.
Download¶
GET /v2/documents/{documentId}/download returns a time-limited URL:
{ "downloadUrl": "https://.../welcome.pdf?...X-Amz-Expires=3600..." }
- The URL is valid for 1 hour. Fetch on demand; don't store it. It is a bearer URL — anyone holding it can download until it expires.
- For processed PDFs you get the QR-stamped copy, exactly what is printed and shipped — not your original upload.
- There is no status gate: downloading a still-
PENDINGdocument returns a200whose URL then fails, because the file isn't in place yet. Wait for confirm. publicUrlon the document is reserved and currently alwaysnull— use the download endpoint.
Delete¶
DELETE /v2/documents/{documentId} hard-deletes the record and its stored files (original and stamped copy) and returns 204.
- A document with any assignment cannot be deleted — you get
409. Delete its assignments first (GET /v2/documents/{documentId}/assignmentsto find them). - Deleting is how you reclaim slots under the 50-document cap, including abandoned
PENDINGrecords.
Assignments¶
An assignment attaches an ACTIVE document to a scope with a delivery mode. Create one with POST /v2/documents/{documentId}/assign:
curl -X POST https://api.fulfillment.juniperhealth.com/v2/documents/2vXk9qAbCdEfGhIjKlMnOpQrStu/assign \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"scope": "PROTOCOL",
"scopeId": "2vYprotocolKsuid",
"deliveryMode": "DIGITAL_AND_ONE_TIME_PRINT"
}'
Response 201 Created:
{
"id": "2vZassignmentKsuid",
"partnerId": "acme-clinic",
"documentId": "2vXk9qAbCdEfGhIjKlMnOpQrStu",
"scope": "PROTOCOL",
"scopeId": "2vYprotocolKsuid",
"deliveryMode": "DIGITAL_AND_ONE_TIME_PRINT",
"printFulfilled": false,
"createdAt": "2026-07-25T17:20:44.002310Z"
}
Assigning a document that isn't ACTIVE (still PENDING, or a PDF still PROCESSING) returns 400 — wait for ACTIVE first.
Scopes¶
| Scope | Applies to | scopeId |
|---|---|---|
ACCOUNT |
Every order for your account | Ignored — server sets it to your partner ID. Omit it. |
PROTOCOL |
Orders for one specific protocol | Always send the protocol ID. |
PROTOCOL_TEMPLATE |
Protocols created from a template — assignments are carried over when the template is copied | Always send the template ID. |
Two sharp edges:
scopeIdis effectively required forPROTOCOLandPROTOCOL_TEMPLATE. If you omit it, the API does not error — it silently records a junk assignment targeted at your partner ID, which will never match a protocol. Always send it.scopeIdexistence is not validated. A typo'd protocol or template ID is accepted and the assignment simply never fires. Double-check the ID.
Duplicates are allowed: repeated identical assign calls create separate assignments, and each one is delivered/printed independently. Check GET /v2/documents/{documentId}/assignments before creating if you need idempotency.
Delivery modes and printing¶
| Mode | Behavior |
|---|---|
DIGITAL_ONLY |
Available digitally; never printed. |
DIGITAL_AND_PRINT |
A printed copy is included with every matching order. |
DIGITAL_AND_ONE_TIME_PRINT |
Printed with the first matching order only. printFulfilled flips to true after that print and stays true. |
Printing details worth knowing:
- Only stamped PDFs are printed. Printed copies are always the QR-stamped version (the QR is scanned during fulfillment verification). A document without a stamped copy — a non-PDF, or a PDF still
PROCESSING— is never printed, regardless of delivery mode. If print delivery matters, upload a PDF. printFulfilledis read-only and is not reset by changing the delivery mode: switching a fulfilledDIGITAL_AND_ONE_TIME_PRINTassignment to another mode and back will not print again. To print once more, create a new assignment.
Managing assignments¶
GET /v2/documents/{documentId}/assignments— every assignment of the document, all scopes, unpaginated:{ "assignments": [ ... ] }.PATCH /v2/document-assignments/{assignmentId}— body{ "deliveryMode": "..." }. Delivery mode is the only mutable field. To change scope or target, delete and re-create.DELETE /v2/document-assignments/{assignmentId}— removes the assignment (204). The document itself is untouched and becomes deletable once its last assignment is gone.
What a protocol's orders will include¶
GET /v2/protocols/{protocolId}/documents returns the merged view of assignments that apply to orders for that protocol: assignments scoped to that protocol plus all of your ACCOUNT-scope assignments, as { "assignments": [ ... ] }.
An unknown protocol ID is not an error here — you simply get the account-wide assignments back. Don't use this endpoint to validate that a protocol exists.
What a template carries¶
GET /v2/protocol-templates/{templateId}/documents returns the documents attached to one of your protocol templates: its PROTOCOL_TEMPLATE assignments, as { "assignments": [ ... ] }. Account-wide assignments are not included.
Read this route, not the template's documentIds field. The portal attaches a document to a template as an assignment and does not update documentIds. An unknown template ID returns an empty list.
Error reference¶
All domain errors use the standard body {"message", "code"} — branch on code.
| Status | code |
When |
|---|---|---|
400 |
INVALID_FILE_TYPE |
Invalid fileType on create |
400 |
NO_FIELDS_TO_UPDATE |
Empty PATCH body |
400 |
DOCUMENT_NOT_ACTIVE |
Confirm on a non-PENDING document; assign on a non-ACTIVE document |
400 |
INVALID_PRODUCT_ID |
productId filter on the library list is not a whole number |
401 |
— (gateway) or UNAUTHORIZED |
Missing/invalid token, or unrecognized API client |
403 |
— (gateway) | Token lacks a required scope — including any v1 key calling v2 |
404 |
DOCUMENT_NOT_FOUND / ASSIGNMENT_NOT_FOUND |
Unknown ID or another partner's ID; on confirm, also "file not landed yet — retry" |
409 |
DOCUMENT_LIMIT_REACHED |
Create at the 50-document cap |
409 |
DOCUMENT_HAS_ASSIGNMENTS |
Delete of a document that still has assignments |
503 |
DOCUMENT_DOWNLOAD_UNAVAILABLE |
A download link could not be issued right now; retry |
422 |
VALIDATION_FAILED |
Request-body validation failure. details.errors lists each failure by type, e.g. {"message": "Validation failed", "code": "VALIDATION_FAILED", "details": {"errors": [{"type": "missing", "loc": []}]}} |
Good to know¶
- Requests and responses are camelCase (
fileType,deliveryMode,printFulfilled). The one exception:uploadPost.fieldskeys are raw storage form-field names — submit them verbatim. - Budget for the scan delay. Upload → confirm is not synchronous. Build the retry-on-404 loop into your confirm step from day one.
- The cap counts everything. Failed or abandoned uploads leave
PENDINGrecords that count toward the 50-document limit until you delete them. Sweep them periodically. - Downloads serve the stamped copy for processed PDFs. If you need your pristine original, keep your own copy.
- Assignments are cheap and dumb: no dedupe, no target validation, immutable scope. Treat
assigncalls as append-only writes you are responsible for keeping tidy. - Library documents (
/v2/library-documents) are DailyStak-owned and read-only — you can list, filter byproductId, and download them, but you assign only your own documents. See Templates & the DailyStak Library.