Skip to content

Content Upload API

The content upload pipeline lets users upload bundles of text and files. Files are uploaded directly to S3 via presigned URLs — the backend never proxies file data.

All endpoints require authentication.

Upload Flow

sequenceDiagram
    participant C as Client
    participant B as Backend
    participant S3 as S3
    participant CF as CloudFront

    C->>B: POST /api/raw-items/upload
    Note right of B: Create files (pending)
    B-->>C: itemId + presigned URLs

    loop For each attachment
        C->>S3: POST multipart form
        S3-->>C: 204 + ETag + SHA256
    end

    C->>B: POST /api/raw-items/id/confirm
    B->>S3: HeadObject
    S3-->>B: size, etag, sha256
    Note right of B: Update files and item (ready)
    B-->>C: status ready

    C->>B: GET /api/files/fileId/download
    B-->>C: 307 Redirect
    C->>CF: GET signed URL
    CF->>S3: Fetch object (OAC)
    S3-->>CF: File data
    CF-->>C: File data

Endpoints

POST /api/raw-items/upload

Request an upload. The backend creates database records and returns presigned URLs for each attachment.

Request:

{
  "type": "PORTAL_UPLOAD_BUNDLE",
  "mainText": "Today's notes with some attachments",
  "attachments": [
    {
      "filename": "photo.jpg",
      "contentType": "image/jpeg",
      "sizeBytes": 2048000
    },
    {
      "filename": "report.pdf",
      "contentType": "application/pdf",
      "sizeBytes": 512000
    },
    {
      "type": "link",
      "url": "https://example.com/interesting-article"
    }
  ]
}

Response (201):

{
  "itemId": "019e4596-2b4a-7c03-9b7f-7c6939c9564f",
  "attachments": [
    {
      "index": 0,
      "url": "https://inklet-dev.s3.us-east-1.amazonaws.com",
      "fields": {
        "key": "user/1a16f2b9-.../2026/05/019e4596-2b44-....jpg",
        "Content-Type": "image/jpeg",
        "x-amz-checksum-algorithm": "SHA256",
        "x-amz-credential": "AKIA.../20260520/us-east-1/s3/aws4_request",
        "x-amz-algorithm": "AWS4-HMAC-SHA256",
        "x-amz-date": "20260520T130000Z",
        "policy": "eyJleHBpcm...",
        "x-amz-signature": "a1b2c3d4..."
      }
    },
    {
      "index": 1,
      "url": "https://inklet-dev.s3.us-east-1.amazonaws.com",
      "fields": { "..." }
    }
  ],
  "expiresAt": "2026-05-20T13:47:09Z"
}

Uploading to S3

The client must construct a multipart/form-data POST request to the url. Include all fields as form fields first, then append the file as the last field named file.

const form = new FormData();
for (const [key, value] of Object.entries(fields)) {
  form.append(key, value);
}
form.append('file', fileBlob);
await fetch(url, { method: 'POST', body: form });

Expiration

The presigned URLs expire after 15 minutes (indicated by expiresAt). Upload all files before this time.

Errors:

Status Reason
400 Missing type, unsupported type, or invalid content type
413 File exceeds 10 MB size limit

Allowed content types (for file attachments):

image/png, image/jpeg, image/gif, image/webp, image/svg+xml, application/pdf, text/plain, text/markdown, application/json

Link Attachments

Attachments with type: "link" don't require S3 upload. The URL is stored directly in the content JSON. During summary processing, the worker fetches the link content via Jina Reader and stores the raw markdown in content.attach[].raw_content. The LLM summary is stored in summary_content.attachments[].description (without raw_content).


POST /api/raw-items/{id}/confirm

Confirm that all files have been uploaded. The backend verifies each file exists in S3 via HeadObject, retrieves the SHA256 checksum, and updates the database.

Response (200) — all files confirmed:

{
  "itemId": "019e4596-2b4a-...",
  "status": "ready"
}

Response (200) — some files failed:

{
  "itemId": "019e4596-2b4a-...",
  "status": "partial",
  "failed": [1]
}

The failed array contains the indices of attachments that could not be verified. The client can retry uploading those files and call confirm again.

Errors:

Status Reason
404 Item not found
403 Not the item owner
409 Item is not in pending status

GET /api/raw-items

List the authenticated user's items with pagination. Results are ordered by creation time (newest first). The response does not include content or userId fields.

Query parameters:

Parameter Description Default
status Filter by status: pending, ready, partial all
page Page number (1-based) 1
limit Items per page (max 50) 20

Response (200):

{
  "items": [
    {
      "id": "019e4596-2b4a-...",
      "type": "PORTAL_UPLOAD_BUNDLE",
      "format": "v0",
      "status": "ready",
      "processStatus": "RAW",
      "createdAt": "2026-05-20T13:32:09Z",
      "updatedAt": "2026-05-20T13:32:15Z"
    }
  ],
  "total": 42,
  "page": 1,
  "limit": 20
}

GET /api/raw-items/{id}

Get a single item. Returns 403 if the item belongs to another user.


GET /api/raw-items/stats

Weekly processing stats for the authenticated user — counts of raw items by processStatus over the last 7 days.

Response (200):

{
  "since": "2026-05-31T08:00:00Z",
  "RAW": 3,
  "INGESTED": 1,
  "READY": 12,
  "FAILED": 2
}
Field Description
since Start of the 7-day window (RFC3339)
RAW Items not yet ingested
INGESTED Items decomposed into content blocks, processing
READY Items whose blocks all summarized
FAILED Items where at least one content block failed

GET /api/files/{fileId}/download

Returns a 307 redirect to a CloudFront signed URL (expires in 15 minutes). The client follows the redirect to download the file.

Only the file owner can access this endpoint. The file must be in ready status.

Errors:

Status Reason
404 File not found
403 Not the file owner
409 File is not ready

S3 Object Key Format

user/{userId}/{year}/{month}/{fileId}.{ext}

Example:
user/1a16f2b9-3b9d-4e57-9272-6a03f0f2274e/2026/05/019e4596-2b44-7c2b-8e36-d367ecab52b7.txt

Content JSON Schema

The content field of a PORTAL_UPLOAD_BUNDLE item (format v0):

{
  "type": "PORTAL_UPLOAD_BUNDLE",
  "format": "v0",
  "upload_at": "2026-05-20T13:32:09Z",
  "main_text": "The user's text content",
  "attach": [
    {
      "type": "image",
      "mime": "image/jpeg",
      "file_id": "019e4596-2b44-7c2b-8e36-d367ecab52b7"
    },
    {
      "type": "doc",
      "mime": "application/pdf",
      "file_id": "019e4596-3c55-7d3c-9f47-e478fdb63c8a"
    },
    {
      "type": "link",
      "url": "https://example.com/article",
      "raw_content": "# Article Title\n\nFull markdown content fetched by Jina Reader..."
    }
  ]
}

Attachment types:

Type Fields Description
image mime, file_id Image file uploaded to S3
doc mime, file_id Document file uploaded to S3
link url, raw_content URL link. raw_content is added by the summary worker after fetching via Jina Reader

File Lifecycle

pending → ready     (normal: upload + confirm)
pending → failed    (cleanup: no upload after 1 hour)
ready   → deleted   (user deletes)
deleted → purged    (cleanup: S3 object removed after 7 days)

Process Status

After an item's upload status becomes ready, the processStatus field tracks the content pipeline:

processStatus Description
RAW Just uploaded, not yet ingested
INGESTED Decomposed into content blocks; download/summary in progress
READY All content blocks summarized
FAILED At least one content block failed

The transition is RAW → INGESTED → READY. If any content block fails (download or summary), the item is set to FAILED.