Attachments

Order attachments let you associate files with an order: invoices, CFDI (XML/PDF), shipping labels, delivery evidence, and more. Files are stored in S3 and accessed via short-lived presigned URLs — they never travel as base64 inside the API's JSON.

Response envelope

Every successful single-resource response follows { "data": {...} }. Errors are always { "error": { "code", "message", "details?" } }.

Real catalog of type (11 values)

CategoryValues
documentinvoice, ticket, cfdi-xml, cfdi-pdf, shipping-guide, customs, receipt, other
evidencepacking-photo, packing-video, condition-photo

Don't confuse it with the old catalog

Values like cfdi, shipping_guide, or custom don't exist — the real catalog uses cfdi-xml/cfdi-pdf, shipping-guide (hyphenated), and other, respectively.

Limits by category

CategoryMax sizeMax per order
document10 MB10
evidence200 MB30

Upload flow

Uploads use a two-step flow with a presigned URL, so the file bytes travel directly to S3 without passing through Fenicia's servers.

1

Request a presigned upload URL

Call POST /orders/{orderId}/attachments/upload-url. You receive a short-lived uploadUrl and an s3Key.

2

Upload the file to S3 with PUT

Upload the file bytes directly to the uploadUrl, using exactly the Content-Type you declared in the previous step.

3

Confirm the attachment

Call POST /orders/{orderId}/attachments with the same s3Key to register the attachment against the order.

Request an upload URL

Generates a presigned S3 URL to upload a file. Does not upload the file — it only prepares the destination.

orderIdstringrequired

Order ID

typestringrequired

Attachment type (see catalog above).

filenamestringrequired

Original filename.

contentTypestringrequired

MIME type of the file (e.g. application/pdf).

{ "type": "invoice", "filename": "factura-1001.pdf", "contentType": "application/pdf" }
200
{
  "data": {
    "uploadUrl": "https://fenicia-order-attachments-prod.s3.us-east-2.amazonaws.com/attachments/...?X-Amz-Signature=...",
    "s3Key": "attachments/65f3a1b2c4d5e6f7a8b9c0d1/invoice-2026-04-11.pdf",
    "expiresIn": 900
  }
}

Required permission: orders:update

curl -X POST https://api.fenicia.io/orders/65f3a1b2c4d5e6f7a8b9c0d1/attachments/upload-url \
  -H "Authorization: Bearer fkapi_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"type":"invoice","filename":"factura-1001.pdf","contentType":"application/pdf"}'

Confirm the attachment

Registers the attachment against the order, after the file has been uploaded to S3 with the presigned URL.

orderIdstringrequired

Order ID

s3Keystringrequired

The same s3Key returned by upload-url.

typestringrequired

Attachment type (see catalog above).

filenamestringrequired

Original filename.

contentTypestringrequired

MIME type of the file.

{
  "s3Key": "attachments/65f3a1b2c4d5e6f7a8b9c0d1/invoice-2026-04-11.pdf",
  "type": "invoice",
  "filename": "factura-1001.pdf",
  "contentType": "application/pdf"
}
201
{
  "data": {
    "attachment": {
      "_id": "65f3a1b2c4d5e6f7a8b9c0e5",
      "type": "invoice",
      "filename": "factura-1001.pdf",
      "contentType": "application/pdf",
      "size": 128340,
      "uploadedAt": "2026-04-11T14:30:00.000Z"
    }
  }
}

Required permission: orders:update

The object is wrapped in attachment

The created attachment travels under data.attachment, not directly under data. If you destructure the response, remember that extra level of nesting.

curl -X POST https://api.fenicia.io/orders/65f3a1b2c4d5e6f7a8b9c0d1/attachments \
  -H "Authorization: Bearer fkapi_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"s3Key":"attachments/65f3a1b2c4d5e6f7a8b9c0d1/invoice-2026-04-11.pdf","type":"invoice","filename":"factura-1001.pdf","contentType":"application/pdf"}'

List an order's attachments

Lists all attachments registered on the order.

orderIdstringrequired

Order ID

200
{
  "data": {
    "attachments": [
      {
        "_id": "65f3a1b2c4d5e6f7a8b9c0e5",
        "type": "invoice",
        "filename": "factura-1001.pdf",
        "contentType": "application/pdf",
        "size": 128340,
        "uploadedAt": "2026-04-11T14:30:00.000Z"
      }
    ],
    "count": 1
  }
}

Required permission: orders:read

curl https://api.fenicia.io/orders/65f3a1b2c4d5e6f7a8b9c0d1/attachments \
  -H "Authorization: Bearer fkapi_your_api_key"

Get an attachment's download URL

Returns a presigned URL to download a specific attachment.

orderIdstringrequired

Order ID

attachmentIdstringrequired

Attachment ID

200
{
  "data": {
    "downloadUrl": "https://fenicia-order-attachments-prod.s3.us-east-2.amazonaws.com/attachments/...?X-Amz-Signature=...",
    "expiresIn": 900
  }
}
404
{ "error": { "code": "attachments:attachment_not_found", "message": "Attachment not found" } }

Required permission: orders:read

Don't confuse it with the singular /attachment route

GET /orders/{orderId}/attachment (singular, no ID) is a legacy route registered on the router but it always responds 501 — it's a disabled stub left over from a migration, not a functional endpoint. Always use the plural route with {attachmentId} documented here.

DOWNLOAD_URL=$(curl -s https://api.fenicia.io/orders/65f3a1b2c4d5e6f7a8b9c0d1/attachments/65f3a1b2c4d5e6f7a8b9c0e5 \
  -H "Authorization: Bearer fkapi_your_api_key" \
  | jq -r '.data.downloadUrl')
 
curl -o factura.pdf "$DOWNLOAD_URL"

Delete an attachment

Deletes an attachment from the order and its file in S3.

orderIdstringrequired

Order ID

attachmentIdstringrequired

Attachment ID

200
{ "data": { "success": true, "message": "Attachment deleted" } }

Required permission: orders:update

200 with a body, not 204

This endpoint responds with 200 and { "data": { "success": true, "message": "..." } } — it is not a bodyless 204. Check data.success instead of relying solely on the status code.

curl -X DELETE https://api.fenicia.io/orders/65f3a1b2c4d5e6f7a8b9c0d1/attachments/65f3a1b2c4d5e6f7a8b9c0e5 \
  -H "Authorization: Bearer fkapi_your_api_key"

Errors

CodeStatusDescription
attachments:invalid_content_type400The declared contentType doesn't match what's expected for the type.
attachments:invalid_attachment_type400The type is not in the real catalog (see table above).
attachments:validation_failed400The body doesn't satisfy the expected schema.
attachments:attachment_limit_exceeded400The maximum number of attachments per order was reached for that category.
attachments:file_not_found400The file was not found in S3 when confirming the attachment (did you skip the upload step?).
attachments:unauthorized403The API key doesn't have the required permission.
attachments:order_not_found404No order with that ID exists in your tenant.
attachments:attachment_not_found404No attachment with that ID exists on that order.
attachments:delete_failed500Failed to delete the file in S3 or the attachment record.

Tip

Size and count limits are per category (document vs. evidence), not a single global limit — review the limits table above before uploading large evidence files.

Next steps