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

# Attachments API: Upload and Manage Files

> Upload, confirm, list, retrieve signed URLs, and delete attachments on invoices, bank movements, and ledger entries using the Sintropix ERP HTTP API.

<Info>
  Source controllers: [`invoice-attachment.controller.ts`](https://github.com/sintropix/monorepo/blob/main/apps/erp-backend/src/attachment/invoice-attachment.controller.ts), [`bank-movement-attachment.controller.ts`](https://github.com/sintropix/monorepo/blob/main/apps/erp-backend/src/attachment/bank-movement-attachment.controller.ts), [`ledger-entry-attachment.controller.ts`](https://github.com/sintropix/monorepo/blob/main/apps/erp-backend/src/attachment/ledger-entry-attachment.controller.ts).
</Info>

The Attachments API lets you associate files with invoices, bank movements, and general ledger entries. Every host follows the same two-phase upload pattern: request a presigned S3 URL, PUT the file from your client, then confirm the upload so the record is created. You can list attachments, fetch a temporary download URL, and remove an attachment when it is no longer needed.

All three hosts share the same five endpoints. Replace the host path segment with the resource you are targeting.

## Supported hosts

| Host          | Base path                                                             |
| ------------- | --------------------------------------------------------------------- |
| Invoice       | `POST /api/entities/:entityId/invoices/:invoiceId/attachments`        |
| Bank movement | `POST /api/entities/:entityId/bank-movements/:movementId/attachments` |
| Ledger entry  | `POST /api/entities/:entityId/ledger/entries/:entryId/attachments`    |

## Two-phase upload flow

1. **Presign**: `POST …/presign` with the file metadata. The server returns an `attachmentId` and a signed `url`.
2. **Upload**: `PUT` the file bytes to the signed URL from your client (not through the ERP API).
3. **Confirm**: `POST …/confirm` with the `attachmentId` and display `filename`. The server reads the stored object metadata and creates the attachment record.

Accepted content types: `application/pdf`, `image/png`, `image/jpeg`, `application/xml`, `text/xml`, `text/csv`, `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`. Maximum file size is 25 MB.

## Presign an upload

Request a presigned S3 URL and an attachment ID before uploading file bytes.

`POST /api/entities/:entityId/invoices/:invoiceId/attachments/presign`

### Path parameters

| Parameter   | Type | Description                                                   |
| ----------- | ---- | ------------------------------------------------------------- |
| `entityId`  | UUID | Entity that owns the parent resource.                         |
| `invoiceId` | UUID | Parent invoice (or `movementId` / `entryId` for other hosts). |

### Body parameters

| Parameter     | Type    | Description                                                         |
| ------------- | ------- | ------------------------------------------------------------------- |
| `filename`    | string  | Display filename for the attachment. Required, minimum 1 character. |
| `contentType` | enum    | One of the accepted MIME types listed above.                        |
| `byteSize`    | integer | File size in bytes. Must be positive and at most 25 MB.             |

### Response

Returns `PresignResult`.

```json theme={null}
{
  "attachmentId": "0192a3b4-c5d6-7e8f-9a0b-1c2d3e4f5a6b",
  "url": "https://<r2-host>/<bucket>/<object-key>?X-Amz-Algorithm=..."
}
```

### Example

```bash theme={null}
curl -X POST "https://<your-erp-backend-host>/api/entities/$ENTITY_ID/invoices/$INVOICE_ID/attachments/presign" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $SINTROPIX_API_KEY" \
  -d '{
    "filename": "invoice-001.pdf",
    "contentType": "application/pdf",
    "byteSize": 142000
  }'
```

## Confirm an upload

After PUTting the file to the presigned URL, confirm the upload to create the attachment record and link it to the host resource.

`POST /api/entities/:entityId/invoices/:invoiceId/attachments/confirm`

### Path parameters

| Parameter   | Type | Description                                                   |
| ----------- | ---- | ------------------------------------------------------------- |
| `entityId`  | UUID | Entity that owns the parent resource.                         |
| `invoiceId` | UUID | Parent invoice (or `movementId` / `entryId` for other hosts). |

### Body parameters

| Parameter      | Type   | Description                                      |
| -------------- | ------ | ------------------------------------------------ |
| `attachmentId` | UUID   | The ID returned by the presign step.             |
| `filename`     | string | Display filename. Required, minimum 1 character. |

### Response

Returns `AttachmentWithoutObjectKey`.

```json theme={null}
{
  "id": "0192a3b4-c5d6-7e8f-9a0b-1c2d3e4f5a6b",
  "filename": "invoice-001.pdf",
  "contentType": "application/pdf",
  "byteSize": 142000,
  "createdAt": "2025-01-15T09:23:00.000Z",
  "updatedAt": "2025-01-15T09:23:00.000Z"
}
```

### Example

```bash theme={null}
curl -X POST "https://<your-erp-backend-host>/api/entities/$ENTITY_ID/invoices/$INVOICE_ID/attachments/confirm" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $SINTROPIX_API_KEY" \
  -H "x-audit-actor: my-agent" \
  -d '{
    "attachmentId": "0192a3b4-c5d6-7e8f-9a0b-1c2d3e4f5a6b",
    "filename": "invoice-001.pdf"
  }'
```

## List attachments

Retrieve all attachments linked to a host resource.

`GET /api/entities/:entityId/invoices/:invoiceId/attachments`

### Path parameters

| Parameter   | Type | Description                                                   |
| ----------- | ---- | ------------------------------------------------------------- |
| `entityId`  | UUID | Entity that owns the parent resource.                         |
| `invoiceId` | UUID | Parent invoice (or `movementId` / `entryId` for other hosts). |

### Response

Returns an array of `AttachmentWithoutObjectKey`.

### Example

```bash theme={null}
curl "https://<your-erp-backend-host>/api/entities/$ENTITY_ID/invoices/$INVOICE_ID/attachments" \
  -H "x-api-key: $SINTROPIX_API_KEY"
```

## Get a download URL

Generate a temporary signed URL to download a specific attachment.

`GET /api/entities/:entityId/invoices/:invoiceId/attachments/:attachmentId/url`

### Path parameters

| Parameter      | Type | Description                                                   |
| -------------- | ---- | ------------------------------------------------------------- |
| `entityId`     | UUID | Entity that owns the parent resource.                         |
| `invoiceId`    | UUID | Parent invoice (or `movementId` / `entryId` for other hosts). |
| `attachmentId` | UUID | Attachment to retrieve.                                       |

### Response

Returns `AttachmentUrl`.

```json theme={null}
{
  "url": "https://<r2-host>/<bucket>/<object-key>?X-Amz-Algorithm=..."
}
```

### Example

```bash theme={null}
curl "https://<your-erp-backend-host>/api/entities/$ENTITY_ID/invoices/$INVOICE_ID/attachments/$ATTACHMENT_ID/url" \
  -H "x-api-key: $SINTROPIX_API_KEY"
```

## Delete an attachment

Remove an attachment and its link to the host resource. The response has no body.

`DELETE /api/entities/:entityId/invoices/:invoiceId/attachments/:attachmentId`

### Path parameters

| Parameter      | Type | Description                                                   |
| -------------- | ---- | ------------------------------------------------------------- |
| `entityId`     | UUID | Entity that owns the parent resource.                         |
| `invoiceId`    | UUID | Parent invoice (or `movementId` / `entryId` for other hosts). |
| `attachmentId` | UUID | Attachment to delete.                                         |

### Response

`204 No Content`

### Example

```bash theme={null}
curl -X DELETE "https://<your-erp-backend-host>/api/entities/$ENTITY_ID/invoices/$INVOICE_ID/attachments/$ATTACHMENT_ID" \
  -H "x-api-key: $SINTROPIX_API_KEY" \
  -H "x-audit-actor: my-agent"
```

## Response schemas

| Schema                       | Source                    | Description                                      |
| ---------------------------- | ------------------------- | ------------------------------------------------ |
| `PresignResult`              | `@sintropix/api-contract` | `{ attachmentId: UUID, url: URL }`               |
| `AttachmentWithoutObjectKey` | `@sintropix/api-contract` | Attachment row without the internal `objectKey`. |
| `AttachmentUrl`              | `@sintropix/api-contract` | `{ url: URL }` temporary download link.          |

## Status codes

| Code  | Meaning                                                                |
| ----- | ---------------------------------------------------------------------- |
| `200` | Success (GET, POST presign/confirm, GET url).                          |
| `204` | Success, no content (DELETE).                                          |
| `400` | Validation error on request body or path parameter.                    |
| `401` | Unauthenticated.                                                       |
| `403` | Forbidden: not a member of the entity, or not staff.                   |
| `404` | Entity, host resource, or attachment not found.                        |
| `409` | Attachment already exists, or content type / size mismatch on confirm. |
| `429` | Rate limit exceeded (5000 requests per hour per API key).              |
