Attachments use a two-step upload flow:
- Create an upload slot.
- PUT the bytes to
upload_url.
- 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
| Rule | Value |
|---|
| Count | 2 or more media parts |
| Supported image types | image/jpeg, image/png, image/gif, image/webp, image/heic, image/heif |
| Per-file size | 100 MB max for registered uploads |
| Rendering | Chert requests grouped delivery. Apple devices decide the final collage or stack presentation. |
| 1 image | Drop parallel_attachments — a single media part renders as a normal image bubble. |
| Large batches | Split into multiple sends when you want predictable recipient UX. |
| Mixed media (image + video) | Delivery is provider/device-dependent. Send separately when presentation matters. |
| Optional caption | A text part is sent as its own text bubble; same-bubble captions are not guaranteed. |
Without parallel_attachments | Multiple media parts render as N separate bubbles. |
Errors
| Code | HTTP | When |
|---|
1003 | 400 | Missing required field. |
1002 | 400 | Invalid part shape or MIME mismatch. |
3002 | 503 | No eligible sender line available in your project. |
4001 | 502 | Delivery failed downstream. |
Supported uploads
| Type | MIME examples |
|---|
| Images | image/jpeg, image/png, image/gif, image/heic, image/heif, image/webp |
| Video | video/mp4, video/quicktime |
| Audio | audio/mpeg, audio/m4a, audio/wav, audio/aac |
| Documents | application/pdf |
The public API validates the declared file size and rejects unsupported MIME types. Upload slots expire after 15 minutes.
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
| Caveat | What to do |
|---|
| Group create with media or rich links | Create the group with text first, then send media or rich links with POST /api/v1/chats/{id}/messages. |
See also