Skip to main content
Developers

Uploading Documents via API (with cURL examples)

Single uploads, batch uploads, resumable uploads for large files — with examples in cURL, .NET, and TypeScript.

Uploading Documents via API (with cURL examples)

Three upload modes:

Mode File size When to use
Direct multipart < 100 MB Most cases
Batch Up to 2 GB Many files at once
Resumable Any size Unreliable networks, very large files

Direct multipart upload

curl https://yourtenant.papyrus.io/api/v1/documents \
  -X POST \
  -H "Authorization: Bearer pk_live_xxxxxxxxxxxx" \
  -H "X-Tenant-Id: yourtenant" \
  -F "file=@invoice.pdf" \
  -F "folderId=11111111-1111-1111-1111-111111111111" \
  -F "tags=invoice,acme,2026-q2" \
  -F "classification=Invoice"

folderId is optional — defaults to the tenant's inbox folder. classification is optional — defaults to AI auto-classify.

.NET example

using System.Net.Http.Headers;

var http = new HttpClient { BaseAddress = new Uri("https://yourtenant.papyrus.io/") };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
http.DefaultRequestHeaders.Add("X-Tenant-Id", tenantSlug);

using var content = new MultipartFormDataContent();
using var fileStream = File.OpenRead("invoice.pdf");
content.Add(new StreamContent(fileStream), "file", "invoice.pdf");
content.Add(new StringContent(folderId.ToString()), "folderId");
content.Add(new StringContent("invoice,acme,2026-q2"), "tags");

var response = await http.PostAsync("/api/v1/documents", content);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ApiResponse<DocumentResponse>>();

TypeScript / Node.js example

import { createReadStream } from 'fs';
import FormData from 'form-data';

const form = new FormData();
form.append('file', createReadStream('invoice.pdf'));
form.append('folderId', '11111111-1111-1111-1111-111111111111');
form.append('tags', 'invoice,acme,2026-q2');

const response = await fetch('https://yourtenant.papyrus.io/api/v1/documents', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${apiKey}`,
    'X-Tenant-Id': tenantSlug,
    ...form.getHeaders()
  },
  body: form
});

const result = await response.json();
console.log(result.data.id);

Resumable upload (large files)

For files over 100 MB or unreliable networks, use the resumable upload protocol:

# 1. Initiate
SESSION=$(curl -X POST https://yourtenant.papyrus.io/api/v1/uploads \
  -H "Authorization: Bearer $KEY" -H "X-Tenant-Id: $TENANT" \
  -d '{"fileName":"large-archive.zip","fileSize":2147483648,"chunkSize":10485760}' \
  -H "Content-Type: application/json" | jq -r '.data.sessionId')

# 2. Upload chunks (parallel ok)
curl -X PUT "https://yourtenant.papyrus.io/api/v1/uploads/$SESSION/chunks/0" \
  -H "Authorization: Bearer $KEY" -H "X-Tenant-Id: $TENANT" \
  --data-binary @chunk-0.bin

# ... continue for all chunks ...

# 3. Finalise
curl -X POST "https://yourtenant.papyrus.io/api/v1/uploads/$SESSION/finalize" \
  -H "Authorization: Bearer $KEY" -H "X-Tenant-Id: $TENANT"

Sessions live for 24 hours. Chunks are deduplicated by hash, so retries of identical chunks don't re-upload bytes.

Idempotency

Pass Idempotency-Key: <uuid> on any upload to prevent duplicate creation on retry:

curl https://yourtenant.papyrus.io/api/v1/documents \
  -X POST \
  -H "Idempotency-Key: 7f3e8d2a-1234-5678-90ab-cdef12345678" \
  -H "Authorization: Bearer $KEY" -H "X-Tenant-Id: $TENANT" \
  -F "file=@invoice.pdf"

Same key within 24 hours returns the same document, regardless of how many times you retry.

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please retry or reload the page.