Skip to content

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. fileType must be one of exactly five MIME types: application/pdf, application/vnd.openxmlformats-officedocument.wordprocessingml.document (DOCX), image/png, image/jpeg, or image/webp. Anything else is a 400.
  • 50-document cap. Each partner may hold at most 50 documents — all statuses count, including abandoned PENDING records. At the cap, create returns 409. Free slots with DELETE /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) and productName are optional associations; productId is 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 fields programmatically. 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 stays PENDING; delete it and create a new one.
  • Size limits. The signed policy enforces 1 byte – 10 MB and pins the Content-Type to the fileType you declared. An out-of-range file is rejected at upload time.
  • uploadUrl is deprecated. It is a bare presigned PUT kept for backward compatibility and slated for removal. New integrations must use uploadPost.

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: status goes straight to ACTIVE and fileSizeBytes is populated. Done.
  • PDFs: status becomes PROCESSING while every page is stamped with a small QR code (used to verify the right document ships with the right order). Stamping typically takes seconds; poll GET /v2/documents/{documentId} until status is ACTIVE.

Confirm has two semantics you must handle:

  • 404 means "not yet" — retry. Because scanning is asynchronous, confirming immediately after upload can return 404 even though the upload succeeded. Retry with backoff. A file that never becomes confirmable (still 404 after 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 PENDING document can be confirmed. A repeat confirm — or a confirm during/after processing — returns 400 (the message reads Document {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
}
  • limit is 1–100 (default 50). Out-of-range values are silently reset to 50 rather than rejected.
  • cursor is opaque — pass it back verbatim; never parse it. An invalid cursor is silently ignored and the listing restarts from the first page.
  • Treat cursor: null as the terminator, not a short page. A page can report hasMore: true and the next fetch returns documents: [] with cursor: null. Loop until cursor is null.

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: null clears the description.
  • Never send name: null — name is 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-PENDING document returns a 200 whose URL then fails, because the file isn't in place yet. Wait for confirm.
  • publicUrl on the document is reserved and currently always null — 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}/assignments to find them).
  • Deleting is how you reclaim slots under the 50-document cap, including abandoned PENDING records.

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:

  • scopeId is effectively required for PROTOCOL and PROTOCOL_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.
  • scopeId existence 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.
  • printFulfilled is read-only and is not reset by changing the delivery mode: switching a fulfilled DIGITAL_AND_ONE_TIME_PRINT assignment 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.fields keys 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 PENDING records 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 assign calls 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 by productId, and download them, but you assign only your own documents. See Templates & the DailyStak Library.