Skip to main content
Attachments use a two-step upload flow:
  1. Create an upload slot.
  2. PUT the bytes to upload_url.
  3. Send a chat message with a media part through POST /api/v1/chats/{id}/messages.

1. Create an upload slot

curl -X POST https://console.trychert.com/api/v1/attachments \
  -H "Authorization: Bearer $CHERT_SIGNING_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "filename": "invoice.pdf",
    "content_type": "application/pdf",
    "size_bytes": 204800
  }'
{
  "attachment_id": "9f3b27a8c14d4a6f8e2b91d4c5e6f7a8",
  "upload_url": "https://console.trychert.com/api/v1/attachments/9f3b27a8c14d4a6f8e2b91d4c5e6f7a8/upload",
  "required_headers": {
    "authorization": "Bearer <your-signing-secret>",
    "content-type": "application/pdf"
  },
  "expires_at": "2026-05-02T18:15:00Z"
}

2. Upload bytes

curl -X PUT "$UPLOAD_URL" \
  -H "Authorization: Bearer $CHERT_SIGNING_SECRET" \
  -H "Content-Type: application/pdf" \
  --data-binary @invoice.pdf
HMAC is not supported on the binary upload route. Use bearer auth.
Single PUTs accept up to 100 MB per file. Chert streams the body straight through to storage — there is no in-memory buffering bottleneck, so large uploads scale with your network throughput. Set Content-Length on the PUT so we can fast-reject a body that exceeds the size you registered.

3. Send the attachment

curl -X POST "https://console.trychert.com/api/v1/chats/CHAT_ID/messages" \
  -H "Authorization: Bearer YOUR_SIGNING_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "message": {
      "parts": [
        { "type": "text",  "value": "See attached." },
        { "type": "media", "attachment_id": "att_abc..." }
      ]
    }
  }'

Send multiple images as one bubble

For multiple uploaded media parts that should arrive as one grouped delivery rather than N back-to-back bubbles, send a normal chat message and set parallel_attachments: true. Chert requests grouped delivery; Apple devices decide the final collage, stack, or attachment presentation. The shape is the same parts[] you use for every other chat send. The only difference is the top-level parallel_attachments: true flag.
curl -X POST "https://console.trychert.com/api/v1/chats/$CHAT_ID/messages" \
  -H "Authorization: Bearer $CHERT_SIGNING_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "parallel_attachments": true,
    "message": {
      "parts": [
        { "type": "media", "attachment_id": "att_aaa..." },
        { "type": "media", "attachment_id": "att_bbb..." },
        { "type": "media", "attachment_id": "att_ccc..." }
      ]
    }
  }'
{
  "message": {
    "id":           "46eb1003c8b54b7ea8f1c2b03e9a7d12",
    "chat_id":      "0f2d3c1a-8b4e-4f6a-90d2-1a3b4c5d6e7f",
    "status":       "sent",
    "delivered_at": "2026-05-27T20:18:28.857Z"
  }
}
You can include parallel_attachments on the first touch too — POST /api/v1/chats accepts it inside message.

Rules

RuleValue
Count2 or more media parts
Supported image typesimage/jpeg, image/png, image/gif, image/webp, image/heic, image/heif
Per-file size100 MB max for registered uploads
RenderingChert requests grouped delivery. Apple devices decide the final collage or stack presentation.
1 imageDrop parallel_attachments — a single media part renders as a normal image bubble.
Large batchesSplit into multiple sends when you want predictable recipient UX.
Mixed media (image + video)Delivery is provider/device-dependent. Send separately when presentation matters.
Optional captionA text part is sent as its own text bubble; same-bubble captions are not guaranteed.
Without parallel_attachmentsMultiple media parts render as N separate bubbles.

Errors

CodeHTTPWhen
1003400Missing required field.
1002400Invalid part shape or MIME mismatch.
3002503No eligible sender line available in your project.
4001502Delivery failed downstream.

Supported uploads

TypeMIME examples
Imagesimage/jpeg, image/png, image/gif, image/heic, image/heif, image/webp
Videovideo/mp4, video/quicktime
Audioaudio/mpeg, audio/m4a, audio/wav, audio/aac
Documentsapplication/pdf
The public API validates the declared file size and rejects unsupported MIME types. Upload slots expire after 15 minutes.

Metadata and download

Use GET /api/v1/attachments/{id} to check the registered attachment.
{
  "attachment": {
    "attachment_id": "9f3b27a8c14d4a6f8e2b91d4c5e6f7a8",
    "content_type": "application/pdf",
    "size_bytes": 204800,
    "filename": "invoice.pdf",
    "uploaded_at": "2026-05-02T18:02:00Z",
    "expires_at": "2026-05-02T18:15:00Z"
  }
}
Use GET /api/v1/attachments/{id}/content to stream the binary content. This works for attachments you uploaded and for inbound media ids received in message.received webhooks.
curl "https://console.trychert.com/api/v1/attachments/$ATTACHMENT_ID/content" \
  -H "Authorization: Bearer $CHERT_SIGNING_SECRET" \
  -H "x-chert-tenant: $SLUG" \
  --output attachment.bin
Inbound images and files arrive in data.message.parts[] as media parts:
{
  "type": "media",
  "attachment_id": "in_536f2fe3cafef93946d0bf9b4ac2e3ef",
  "filename": "photo.jpg",
  "mime_type": "image/jpeg",
  "size_bytes": 790394
}

Caveats

CaveatWhat to do
Group create with media or rich linksCreate the group with text first, then send media or rich links with POST /api/v1/chats/{id}/messages.

See also