Attached documents

Upload, attach, and read files for a selected product, customer, supplier, or purchase order.

The upload flow keeps a file private, then attaches it to the exact product, customer, supplier, or purchase order your workflow selected. Product files can also be assigned as the product's current SDS.

Endpoint map

EndpointScopeUse it for
POST /document-uploadsdocuments:uploadPrepare one private file upload and return the values needed to send it.
GET /products/{id}/documentsproducts:read or search:readList document links attached to one visible product. Product responses also include the product SDS pointer when present.
POST /products/{id}/documentsproducts:writeAttach one uploaded file or HTTPS document link to an active product.
PUT /products/{id}/sdsproducts:writeAssign one uploaded PDF or image as the product's current SDS.
GET /customers/{id}/documentscustomers:read or search:readList document links attached to one visible customer.
POST /customers/{id}/documentscustomers:writeAttach one uploaded file or HTTPS document link to an active customer.
GET /purchase-orders/{id}/documentspurchase-order-execution:readList document links attached to one visible purchase order.
POST /purchase-orders/{id}/documentspurchase-order-documents:writeAttach one uploaded file or HTTPS document link to a visible purchase order.
GET /suppliers/{id}/documentssuppliers:read or search:readList document links attached to one visible supplier.
POST /suppliers/{id}/documentssuppliers:writeAttach one uploaded file or HTTPS document link to an active supplier.

Document lists stay scoped to the selected parent and report whether the returned set is truncated. Product, customer, and supplier reads can include archived parents, while purchase-order reads follow the selected order and the acting user's current access.

Read attached documents

Fetch attached documents only after search, a finder, or a detail response has selected the exact parent id.

curl "$SHELFCYCLE_API_BASE_URL/products/product-id/documents" \
  -H "Authorization: Bearer $SHELFCYCLE_API_KEY"
{
  "data": {
    "documents": [
      {
        "type": "document",
        "id": "document-id",
        "fileName": "ACE Spec.pdf",
        "createdAt": "2026-07-02T18:00:00.000Z",
        "urlClass": "shelfcycle",
        "access": {
          "mode": "signed",
          "url": "https://...",
          "expiresAt": "2026-07-02T18:15:00.000Z"
        }
      }
    ],
    "truncated": false,
    "sds": {
      "fileName": "ACE SDS.pdf",
      "access": { "mode": "signed", "url": "https://...", "expiresAt": "2026-07-02T18:15:00.000Z" }
    }
  }
}

ShelfCycle-hosted files return short-lived signed read URLs. External HTTPS links return access.mode: "raw". If an old ShelfCycle file points outside the authenticated org prefix, the row can return access.mode: "unavailable" without a URL.

Upload a private file

Start with the local file's name, size, content type, and lowercase hexadecimal SHA-256 checksum. The file can be from 1 byte through 15 MiB.

General attachments accept PDF; JPG/JPEG, PNG, GIF, WebP, and BMP images; and DOCX, XLSX, and PPTX files. A current Product SDS can be a PDF or one of the supported image types.

FILE_PATH="./ACE-SDS.pdf"
FILE_NAME="$(basename "$FILE_PATH")"
FILE_SIZE="$(wc -c < "$FILE_PATH" | tr -d ' ')"
FILE_SHA256="$(shasum -a 256 "$FILE_PATH" | awk '{print $1}')"

UPLOAD_RESPONSE="$(curl "$SHELFCYCLE_API_BASE_URL/document-uploads" \
  -X POST \
  -H "Authorization: Bearer $SHELFCYCLE_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"fileName\":\"$FILE_NAME\",\"contentType\":\"application/pdf\",\"sizeBytes\":$FILE_SIZE,\"checksumSha256\":\"$FILE_SHA256\"}")"

UPLOAD_URL="$(printf '%s' "$UPLOAD_RESPONSE" | jq -r '.data.upload.url')"
UPLOAD_CONTENT_LENGTH="$(printf '%s' "$UPLOAD_RESPONSE" | jq -r '.data.upload.headers["Content-Length"]')"
UPLOAD_CONTENT_TYPE="$(printf '%s' "$UPLOAD_RESPONSE" | jq -r '.data.upload.headers["Content-Type"]')"
UPLOAD_CHECKSUM="$(printf '%s' "$UPLOAD_RESPONSE" | jq -r '.data.upload.headers["x-amz-checksum-sha256"]')"
DOCUMENT_FILE_NAME="$(printf '%s' "$UPLOAD_RESPONSE" | jq -r '.data.document.fileName')"
DOCUMENT_URL="$(printf '%s' "$UPLOAD_RESPONSE" | jq -r '.data.document.url')"

The response contains two separate values:

  • data.upload tells the client where and how to send the file during the next 10 minutes.
  • data.document is the exact {fileName,url} value to use when attaching the uploaded file or assigning it as a Product SDS.
{
  "data": {
    "type": "document_upload_grant",
    "id": "upload-id",
    "upload": {
      "method": "PUT",
      "url": "https://upload-url",
      "headers": {
        "Content-Length": "124000",
        "Content-Type": "application/pdf",
        "If-None-Match": "*",
        "x-amz-checksum-sha256": "base64-checksum"
      },
      "expiresAt": "2026-07-22T18:10:00.000Z"
    },
    "document": {
      "fileName": "ACE-SDS.pdf",
      "url": "https://shelfcycle-file-url"
    }
  }
}

Send the file to data.upload.url with every returned header exactly as provided. Do not send the ShelfCycle API key with this request.

curl "$UPLOAD_URL" \
  -X PUT \
  --data-binary "@$FILE_PATH" \
  -H "Content-Length: $UPLOAD_CONTENT_LENGTH" \
  -H "Content-Type: $UPLOAD_CONTENT_TYPE" \
  -H "If-None-Match: *" \
  -H "x-amz-checksum-sha256: $UPLOAD_CHECKSUM"

After the file upload succeeds, use the returned data.document value to attach or assign it. Request a fresh upload when the 10-minute upload window has expired.

Attach an uploaded file

Submit the returned data.document value to the selected product, customer, supplier, or purchase-order document route with a stable idempotency key. The record-specific write scope and the acting user's current permission control the attachment.

curl "$SHELFCYCLE_API_BASE_URL/customers/customer-id/documents?dryRun=true" \
  -X POST \
  -H "Authorization: Bearer $SHELFCYCLE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: intake:customer-id:w9-upload" \
  -d "$(jq -n --arg fileName "$DOCUMENT_FILE_NAME" --arg url "$DOCUMENT_URL" '{fileName:$fileName,url:$url}')"
{
  "data": {
    "type": "write_readiness",
    "operation": "customer_documents.create",
    "status": "ready",
    "wouldWrite": false,
    "checks": [
      { "code": "scope_authorized", "status": "passed" },
      { "code": "target_available", "status": "passed" },
      { "code": "idempotency_available", "status": "passed" },
      { "code": "duplicate_check_clear", "status": "passed" }
    ],
    "duplicateCandidates": [],
    "verification": {
      "available": true,
      "path": "/api/v1/customers/customer-id/documents",
      "fallback": "detail_get"
    },
    "requestId": "request-id"
  }
}

Execute the same request without dryRun=true when your integration policy allows it. A successful uploaded-file attachment returns a durable integrity receipt:

{
  "data": {
    "type": "document",
    "id": "document-id",
    "fileName": "ACE-SDS.pdf",
    "createdAt": "2026-08-11T18:00:00.000Z",
    "urlClass": "shelfcycle",
    "access": {
      "mode": "signed",
      "url": "https://...",
      "expiresAt": "2026-08-11T18:15:00.000Z"
    },
    "idempotencyStatus": "created",
    "integrity": {
      "status": "verified",
      "checksumSha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
      "sizeBytes": 124000,
      "contentType": "application/pdf",
      "verifiedAt": "2026-08-11T18:00:00.000Z"
    }
  }
}

integrity.status: "verified" confirms that ShelfCycle matched the stored file's checksum, size, and content type when it attached the file. Reusing the same route, idempotency key, and body returns the existing document row with idempotencyStatus: "replayed" and the same stored integrity receipt, even when the original response was lost.

An external HTTPS link returns integrity.status: "not_verified". An uploaded file attached before integrity receipts were recorded can return integrity.status: "unavailable" on replay. A dry run checks that the upload is ready but creates no receipt; execution verifies the upload again and records the receipt.

The upload scope prepares the file but does not attach it. The matching record write scope controls the association. To receive a readable URL in the create response, the key must also be able to read that target record; otherwise access.mode is unavailable and no URL is returned.

Assign the current Product SDS

Use the same uploaded data.document value with the latest product updatedAt. A dry run is available before assignment.

curl "$SHELFCYCLE_API_BASE_URL/products/product-id/sds?dryRun=true" \
  -X PUT \
  -H "Authorization: Bearer $SHELFCYCLE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "If-Match: 2026-07-22T17:45:00.000Z" \
  -d "$(jq -n --arg fileName "$DOCUMENT_FILE_NAME" --arg url "$DOCUMENT_URL" '{fileName:$fileName,url:$url}')"

Remove dryRun=true to assign the SDS after the readiness response is ready. The response reports the product id, file name, new updatedAt, and whether the SDS was updated, replayed, or repaired. Verify the result with GET /products/{id}/documents; data.sds is the current SDS and data.documents keeps the product's attached-file history.

Attach an existing HTTPS link

The product, customer, supplier, and purchase-order document routes also accept an existing HTTPS file link. Document create accepts exactly fileName and url; the response identifies these links with integrity.status: "not_verified" so callers can distinguish them from verified uploads.

URL rules

Document URLs must use https and cannot contain embedded credentials. External HTTPS links are accepted. ShelfCycle bucket links are accepted only when the bucket key belongs to the authenticated org; signed URL query strings are normalized before duplicate checks.

Exact duplicate URLs on the same parent are blocked with duplicate_document. Requests with fields other than fileName and url are blocked with unsupported_document_field.

Sensitive files

Attached files may contain tax, quality, commercial, or regulatory context. Use the narrowest parent route, keep signed URLs out of logs, and avoid echoing request bodies that contain sensitive source links.

Guardrail

Choose the exact parent record before attaching a file, keep upload and signed read URLs out of logs, and verify the final attachment or Product SDS through that record's document list.