Documents

Before Corvo can print and ship a document, you must upload the PDF or create a tracked presigned upload target. Every shipment references a previously validated `document_key`.

Document metadata object

Returned by upload and confirm

NameTypeRequiredDescription
data.document_keystringYesOpaque key you pass to shipment creation. It is not a public download URL.
data.filenamestringYesNormalized PDF filename stored with the upload.
data.page_countintegerYesValidated page count extracted from the uploaded PDF.
data.size_bytesintegerYesUploaded file size in bytes.

Direct multipart upload

Use direct upload when your backend already has the PDF bytes and does not need a browser-to-S3 handoff.

POST/api/v1/documents/upload

Upload a PDF directly as multipart form data.

Request body

NameTypeRequiredDescription
filefileYesPDF document uploaded as multipart/form-data.
Shell
curl -X POST https://corvo.to/api/v1/documents/upload \
  -H "Authorization: Bearer $CORVO_API_KEY" \
  -F "file=@/path/to/notice.pdf"
Response201Document uploaded
{
  "data": {
    "document_key": "documents/7b2f4a1e-9c3d-4e5f-8a6b-1c2d3e4f5a6b/a8e4f2c1-3b5d-4e7f-9a1b-2c3d4e5f6a7b.pdf",
    "filename": "notice.pdf",
    "page_count": 12,
    "size_bytes": 245760
  }
}
Response400Invalid PDF
{
  "error": {
    "message": "Encrypted PDFs are not supported",
    "code": "ENCRYPTED_PDF"
  }
}

Presigned upload flow

Use the tracked presigned flow when a browser or a separate worker should upload directly to S3. This is a three-step process: create-target, upload to S3, then confirm.

Step 1: Create an upload target

POST/api/v1/documents/upload-url

Create a short-lived S3 form POST target for a PDF upload.

Request body (optional JSON)

NameTypeRequiredDescription
filenamestringNoOriginal PDF filename. Corvo normalizes the name and preserves a `.pdf` extension.

Response schema

NameTypeRequiredDescription
data.upload_idstring (UUID)YesTracked upload ID you later confirm with `/documents/confirm`.
data.upload_urlstringYesS3 form POST target. Step 2 is sent here, not to Corvo.
data.fieldsobjectYesForm fields that must be echoed back exactly to S3.
data.expires_inintegerYesSeconds until the S3 form POST target expires.
Shell
curl -X POST https://corvo.to/api/v1/documents/upload-url \
  -H "Authorization: Bearer $CORVO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"filename":"notice.pdf"}'
Response200Tracked upload target
{
  "data": {
    "upload_id": "550e8400-e29b-41d4-a716-446655440000",
    "upload_url": "https://corvo-documents.s3.amazonaws.com/",
    "expires_in": 3600,
    "fields": {
      "key": "documents/pending/org-id/550e8400-e29b-41d4-a716-446655440000.pdf",
      "Content-Type": "application/pdf",
      "Content-Disposition": "inline; filename=\"notice.pdf\"",
      "Policy": "...",
      "X-Amz-Algorithm": "AWS4-HMAC-SHA256",
      "X-Amz-Credential": "...",
      "X-Amz-Date": "20260309T150000Z",
      "X-Amz-Signature": "..."
    }
  }
}

Step 2: POST the file to S3

This request goes to the returned S3 upload_url, not to Corvo. Include every returned field exactly as given, then append the file part last.

Shell
curl -X POST "$UPLOAD_URL" \
  -F "key=$KEY" \
  -F "Content-Type=application/pdf" \
  -F "Content-Disposition=$CONTENT_DISPOSITION" \
  -F "Policy=$POLICY" \
  -F "X-Amz-Algorithm=$X_AMZ_ALGORITHM" \
  -F "X-Amz-Credential=$X_AMZ_CREDENTIAL" \
  -F "X-Amz-Date=$X_AMZ_DATE" \
  -F "X-Amz-Signature=$X_AMZ_SIGNATURE" \
  -F "file=@/path/to/notice.pdf;type=application/pdf"

Step 3: Confirm the upload

POST/api/v1/documents/confirm

Validate a tracked S3 upload and return a final document key.

Request body

NameTypeRequiredDescription
upload_idstring (UUID)YesTracked upload ID returned by `/documents/upload-url`.
Shell
curl -X POST https://corvo.to/api/v1/documents/confirm \
  -H "Authorization: Bearer $CORVO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"upload_id":"550e8400-e29b-41d4-a716-446655440000"}'
Response200Upload confirmed
{
  "data": {
    "document_key": "documents/7b2f4a1e-9c3d-4e5f-8a6b-1c2d3e4f5a6b/e1f2a3b4-5c6d-4e7f-8a9b-0c1d2e3f4a5b.pdf",
    "filename": "notice.pdf",
    "page_count": 48,
    "size_bytes": 5242880
  }
}
Response410Upload expired
{
  "error": {
    "message": "This upload has expired. Please start a new upload.",
    "code": "UPLOAD_EXPIRED"
  }
}