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.