> ## Documentation Index
> Fetch the complete documentation index at: https://docs.payfi.global/llms.txt
> Use this file to discover all available pages before exploring further.

# Upload Flow

> Secure document upload with Brazil-resident private storage.

PayFi stores production operation documents in a Brazil-resident private S3-compatible bucket. The default production region is AWS `sa-east-1`. Local Docker uses MinIO with the same S3-compatible contract.

The API validates declared content type, extension, and size before returning a presigned URL. Upload registration is blocked until the operation's document requirements are approved. After upload completion, PayFi validates the actual bytes, computes a digest, checks parseability and duplicates, and blocks Analysis processing until the malware scan gate marks the object clean.

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant Client as Client browser / partner UI
    participant Partner as Partner backend
    participant PayFi as PayFi API
    participant Storage as Private storage
    participant Scanner as Malware scan gate
    participant Queue as Analysis queue

    Partner->>PayFi: POST /v1/operations
    PayFi-->>Partner: REQUIREMENTS_PENDING
    PayFi-->>Partner: operation.requirements.ready
    Partner-->>Client: Show approved upload flow
    Client->>PayFi: POST /v1/operations/{operationId}/documents
    PayFi->>PayFi: Validate declared metadata
    PayFi-->>Client: documentId, presigned uploadUrl
    Client->>Storage: PUT file bytes
    Client->>PayFi: POST /v1/upload-sessions/{token}/documents/{documentId}/complete
    PayFi->>Storage: Read and validate object
    PayFi->>PayFi: Check magic bytes, MIME, parseability, SHA-256, duplicate
    PayFi->>Scanner: Scan uploaded object
    Scanner-->>PayFi: clean or rejected
    PayFi->>Queue: Create Analysis job when clean
```

## Why uploads are split into metadata and bytes

The partner or client first registers the document metadata with PayFi. PayFi then returns a short-lived presigned URL for direct upload into private storage.

This keeps large files out of the API process, makes uploads resumable at the UI layer, and gives PayFi a deterministic metadata record before any file bytes are accepted.

## Document registration

Use `POST /v1/operations/{operationId}/documents` with:

* `documentType`: one of the expected document types for the operation.
* `fileName`: the original client-facing file name.
* `contentType`: the declared MIME type.
* `sizeBytes`: expected object size.

PayFi rejects unsafe file names, unsupported content types, invalid extensions, and files larger than the configured limit before issuing a presigned URL. Declared metadata is only the first gate; completion validates the file's magic bytes, effective MIME and parseability against the declaration.

PayFi also rejects document registration while an operation is still in `REQUIREMENTS_PENDING`, `REQUIREMENTS_REVIEW_REQUIRED`, or `REQUIREMENTS_FAILED`. The approved document list is the source of truth for upload.

Register the document metadata:

`POST /v1/operations/{operationId}/documents`

```json theme={null}
{
  "documentType": "invoice",
  "fileName": "invoice-123.pdf",
  "contentType": "application/pdf",
  "sizeBytes": 123456
}
```

```json theme={null}
{
  "documentId": "doc_example",
  "status": "UPLOAD_PENDING",
  "uploadUrl": "https://storage.example.com/presigned-upload",
  "uploadUrlExpiresInSeconds": 300
}
```

## Direct upload

The client uploads the file bytes directly to the returned `uploadUrl`. The upload URL is short-lived and scoped to one object key.

```http theme={null}
PUT https://storage.example.com/presigned-upload
Content-Type: application/pdf
Content-Length: 123456
```

Do not send raw document bytes to any partner webhook, support channel, or review channel. Raw documents stay in private storage and are only read by the analysis worker.

## Retention and residency

Production raw operation documents use the `operation-documents/...` key prefix so storage lifecycle policies can target them without deleting risk-policy source files.

Delete production raw objects immediately after successful processing or terminal failure. During an eligible retry, retain them for no more than 24 hours. Configure the bucket lifecycle for `operation-documents/*` as the same 24-hour defense-in-depth upper bound. Risk-policy source documents and other long-lived reference materials must use separate prefixes and must not be covered by that rule.

The expiry reconciler deletes overdue objects and audits both cleanup success and failure. Replay does not renew retention and returns `REPLAY_INPUT_UNAVAILABLE` when no authorized input snapshot remains.

Do not store raw partner documents on local SSDs or unmanaged support systems. Long-term model-improvement data must be anonymized and allowed by the customer DPA.

## Completion gate

After the direct upload, call:

`POST /v1/upload-sessions/{token}/documents/{documentId}/complete`

```json theme={null}
{
  "documentId": "doc_example",
  "status": "CLEAN",
  "operationStatus": "DOCUMENTS_UPLOADED"
}
```

PayFi then:

1. Finds the upload session by token.
2. Confirms the document belongs to the same operation and organization.
3. Reads object metadata from storage.
4. Verifies object size, extension, magic bytes, effective MIME, and parseability.
5. Calculates SHA-256 and detects duplicates within the authorized tenant scope.
6. Runs the malware scan gate in fail-closed mode.
7. Marks the document as `CLEAN` or `REJECTED`.
8. Creates a pending analysis job only for clean, valid documents.

If the scanner is unavailable, times out, or returns an invalid response, PayFi blocks processing. Scanner failure is never converted into `CLEAN`.

## Client remediation

If a document is rejected, the operation does not advance to analysis for that file. The partner should ask the client to upload a corrected document through the approved upload flow.

If only part of the required document set is clean, the operation moves to `DOCUMENTS_PARTIAL`. When every required type is clean, it moves to `DOCUMENTS_UPLOADED` and becomes eligible for processing.
