Gent API Documentation

Messages

Send, receive, and manage email messages and threads. Agent tokens use the token's inbox and scopes; session callers pass inbox_id and are limited to inboxes in their tenant.

Timezones: received_at and sent_at timestamps are returned in the inbox's default_timezone (set via Inbox Settings) as LocalDateTime strings. If no default_timezone is set, they are returned as UTC (Z suffix). The after/before filter params accept any format — naive inputs (no timezone offset) are treated as default_timezone.
POST /v1/messages/ Send a message  scope: email:send → message_id, thread_id
Request — to reply, pass reply_to_message_id; to and subject are derived automatically
{ "reply_to_message_id": "<message-id>", "subject": "Hello Bob", "to": [{"email": "bob@example.com", "name": "Bob"}], "cc": [], "bcc": [], "reply_to": [{"email": "other@example.com"}], "text_body": "Hi Bob,\n\nPlain text.", "html_body": "<p>Hi Bob,</p>", "attachments": [{"blob_id": "G123abc..."}] }
FieldTypeRequiredDescription
reply_to_message_idstringoptionalSet to reply; omit for new message.
subjectstringoptionalDerived from original on reply.
toarrayconditionalRequired unless replying.
ccarrayoptional
bccarrayoptional
reply_toarrayoptional
text_bodystringconditionalAt least one of.
html_bodystringconditionalAt least one of text_body or html_body is required.
attachmentsarrayoptional
201 Created
{ "message_id": "<message-id>", "thread_id": "<thread-id>" } { "queued": true, "approval_id": "<action-id>" }
→ Message send field reference
POST /v1/messages/broadcast/ Broadcast to a contact label  scope: email:send
Request
{ "label_id": "<label-id>", "subject": "Monthly update", "text_body": "Hello everyone...", "html_body": "<p>Hello...</p>" }
FieldTypeRequiredDescription
label_idstringrequiredMust be type contact or any.
subjectstringoptional
text_bodystringconditionalAt least one of text_body / html_body required.
html_bodystringoptional
202 Accepted
{ "queued": true, "broadcast_id": "<broadcast-id>", "label_id": "<label-id>", "label_name": "newsletter" }
poll GET /v1/messages/broadcasts/{id}/ for delivery status.
GET /v1/messages/broadcasts/{broadcast_id}/ Broadcast delivery status  scope: email:read
{ "broadcast_id": "<broadcast-id>", "label_id": "<label-id>", "subject": "Monthly update", "status": "sending", "sent_count": 487, "suppressed_count": 13, "delivered_count": 471, "bounced_count": 16, "spam_complaint_count": 2, "open_count": 312, "click_count": 87, "created_at": "2026-05-30T10:00:00Z", "updated_at": "2026-05-30T10:05:22Z" }
FieldTypeRequiredDescription
open_countnumberoptionalPixel loads — see note below.
click_countnumberoptionalTracked link clicks — not deduplicated.
Counters update asynchronously as SMTP delivery events arrive. delivered_count + bounced_count converges to sent_count over minutes to hours depending on recipient mail servers. spam_complaint_count increments when a recipient marks a message from this broadcast as spam.
Open and click tracking: When an HTML body is provided, a 1×1 tracking pixel is automatically injected and all links are rewritten to go through GET /v1/track/click/{link_id}/ before redirecting to the original destination. open_count reflects pixel loads (not unique opens — email prefetch and privacy proxies inflate this figure). click_count reflects link clicks, also not deduplicated. Unsubscribe links are never rewritten. Both counters are 0 for text-only broadcasts.
GET /v1/messages/suppressed/ List suppressed addresses  scope: email:read
[{ "address": "alice@example.com", "source": "bounce:hard", "reason": "hard_bounce", "suppressed_at": "2026-05-28T14:22:00Z" }]
FieldTypeRequiredDescription
reasonstringoptionalOne of: hard_bounce, unsubscribed, spam_complaint.
Hard-bounce suppressions are added automatically when a permanent SMTP failure (5xx) is received for an address. Unsubscribe suppressions are added when a recipient clicks the unsubscribe link in a broadcast.
DELETE /v1/messages/suppressed/ Remove a suppression  scope: email:write
Request — address in body, not URL
{ "address": "alice@example.com" }
204 No Content
Removes the inbox-wide hard-bounce entry for the address — e.g. after a contact updates their email. The address goes in the request body rather than the URL to keep it out of server access logs. Unsubscribe suppressions (label opt-outs) are unaffected.
Tracking endpoints — no auth required
GET /v1/track/open/{broadcast_id}/{token}/ Open-tracking pixel
# Returns a 1×1 transparent GIF. Called automatically by email clients when the # HTML body is rendered. Increments open_count on the broadcast record. # Token is an HMAC-SHA256 signature — generated at send time, not guessable. # Always returns 200 with the pixel regardless of token validity to prevent # broadcast-existence enumeration.
GET /v1/track/click/{link_id}/ Click-tracking redirect
# 302 redirect to the original link destination. # link_id is a UUID generated per link at send time — stored server-side so # the destination URL is never in the tracking URL (no open-redirect risk). # Increments click_count on the broadcast record before redirecting. # Returns 404 if the link_id is unknown.
GET /v1/messages/ List messages  scope: email:read
Query params
ParameterDescription
qFull-text search (overrides other filters when set).
folderoptionalAny custom folder name — resolved automatically to the matching folder ID.
mailbox_idFilter by raw folder ID (for custom folders without a standard role).
afteroptionalDatetime string — naive inputs assumed to be in default_timezone.
unreadTrue to return only unread messages.
starredoptionalZero-based offset Default: 0..
limitPage size 1–200 Default: 50..
200 OK
{ "results": [ { "id": "msg_01HX...", "thread_id": "thr_01HX...", "from": [{ "email": "alice@example.com", "name": "Alice" }], "to": [{ "email": "agent@example.com" }], "subject": "Hello", "received_at": "2026-05-20T08:14:00", "size": 4096, "flags": { "read": true, "starred": false }, "label_suggestions": [], "event_suggestion": null } ], "total": 142, "position": 0, "limit": 50 }
FieldTypeRequiredDescription
event_suggestionoptionalOptional// non-null when calendar_suggest feature detects a meeting/appointment.
totalnumberoptionalTotal matching messages across all pages.
positionnumberoptionalZero-based offset of first result returned.
GET /v1/messages/{id}/ Get a message  scope: email:read
{ "id": "msg_01HX...", "thread_id": "thr_01HX...", "from": [{ "email": "alice@example.com", "name": "Alice" }], "to": [{ "email": "agent@example.com" }], "subject": "Hello", "text_body": "Hi there,\n\n...", "html_body": "<p>Hi there,</p>...", "attachments": [], "received_at": "2026-05-20T08:14:00", "size": 4096, "flags": { "read": true, "starred": false }, "label_suggestions": [{ "label_id": "<label-id>", "label_name": "billing", "confidence": 0.84 }], "event_suggestion": { "title": "Team standup", "start": "2024-06-03T10:00:00", "duration": "PT30M", "location": "Conference Room B", "description": "Weekly standup", "participants": [{ "name": "Alice", "email": "alice@example.com" }] } }
FieldTypeRequiredDescription
event_suggestionobjectoptionalNull when no suggestion; populated when calendar_suggest feature is enabled.
GET /v1/messages/threads/{id}/ Get a thread  scope: email:read
{ "id": "thr_01HX...", "email_ids": ["msg_01HX...", "msg_02HX..."] }
GET /v1/messages/threads/{thread_id}/summary/ AI thread summary  scope: email:read
200 OK — cached summary available
{ "summary": "2-3 sentence description of what was discussed", "decisions": ["explicit decision 1"], "action_items": [{ "owner": "email or name", "action": "what they need to do" }], "status": "open", "stale": false, "updated_at": "2026-05-20T08:14:00Z" }
FieldTypeRequiredDescription
statusstringoptionalOne of: open, resolved.
stalebooleanoptionalTrue when new messages arrived after last summarization.
202 Accepted — no summary yet; task enqueued
{ "status": "pending", "thread_id": "<thread-id>" }
Requires the thread_summary feature to be enabled via PUT /v1/enrichment/. stale: true means the summary predates new messages — a fresh run is enqueued automatically. Retry after a few seconds when 202 is returned.
GET /v1/messages/ticketing/reference/ Ticketing builder contract  no auth required
200 OK
{ "resource": "messages.ticketing", "label_groups": [...], "statuses": ["open", "pending_sender", "pending_internal", "resolved", "closed"], "priorities": ["low", "normal", "high", "urgent"], "class_config": { "required_fields": ["label_key"], "fields": [...], "matching": {...} }, "endpoints": {...} }
Use this contract to build lightweight email ticketing controls around inbox threads. It is not a full help desk workspace: ticket state stays attached to the email thread, assignment uses inbox ownership, and activity appears in the contact timeline.
Ticket class labels use keys such as ticket/class/billing. Class labels can open ticket workflow manually; status and priority labels are system-managed and should be changed through ticket endpoints or workflow rules.
GET /v1/messages/threads/ List ticket threads  scope: email:read
Query params
ParameterDescription
ticketRequired. Set to true; current thread listing is ticket-focused.
statusOptional ticket status filter.
limitOptional page size.
200 OK
{ "results": [{ "thread_id": "<thread-id>", "status": "open", "priority": "normal", "class_label_key": "ticket/class/billing" }], "total": 1, "limit": 50 }
GET /v1/messages/threads/{thread_id}/labels/ List thread labels  scope: email:read
{ "results": [...] }
POST /v1/messages/threads/{thread_id}/labels/ Apply a thread label  scope: labels:write
Request body
{ "label_key": "ticket/class/billing", "source": "manual", "confidence": 1.0 }
Pass either label_id or label_key. Source is one of manual, rule, cluster, llm, or system.
DELETE /v1/messages/threads/{thread_id}/labels/{label_id}/ Remove a thread label  scope: labels:write
Returns 204 No Content. Ticket status and priority labels are managed by ticket workflow endpoints.
GET /v1/messages/threads/{thread_id}/links/ List linked threads  scope: email:read
{ "results": [...] }
POST /v1/messages/threads/{thread_id}/fork/ Start a related thread  scope: email:send
Request body
{ "to": [{ "email": "customer@example.com" }], "subject": "New request", "text_body": "Opening a new related thread.", "note": "Split from previous closed ticket", "ticket": { "class_label_key": "ticket/class/support", "priority": "normal" } }
At least one of text_body or html_body is required. The new thread is linked back to the source thread. Use this when a closed ticket receives a separate new request or a different class should be handled as a new ticket.
GET /v1/messages/threads/{thread_id}/ticket/ Get thread ticket  scope: email:read
{ "thread_id": "<thread-id>", "status": "open", "priority": "normal", "class_label_key": "ticket/class/support" }
POST /v1/messages/threads/{thread_id}/ticket/ Open or update a ticket  scope: email:write
Request body
{ "class_label_key": "ticket/class/billing", "priority": "high", "assignee_inbox_id": "<inbox-id>", "due_at": "2026-06-20T17:00:00Z" }
PATCH /v1/messages/threads/{thread_id}/ticket/ Update ticket fields  scope: email:write
Request body — all fields optional
{ "status": "pending_sender", "priority": "urgent", "class_label_key": "ticket/class/billing", "assignee_inbox_id": "<inbox-id>", "due_at": "2026-06-20T17:00:00Z" }
Use this endpoint for active workflow changes and ticket reclassification. Use the dedicated resolve and close endpoints when setting final states because those require a resolution note.
POST /v1/messages/threads/{thread_id}/ticket/resolve/ Resolve a ticket  scope: email:write
Request body
{ "resolution_note": "Customer confirmed this is fixed." }
200 OK
Adds a closing note and transitions the ticket to resolved. If no note is needed, use an empty body.
POST /v1/messages/threads/{thread_id}/ticket/close/ Close a ticket  scope: email:write
Request body
{ "resolution_note": "No further action needed." }
200 OK
Adds a closing note and transitions the ticket to closed. If no note is needed, use an empty body.
POST /v1/messages/threads/{thread_id}/ticket/reopen/ Reopen a ticket  scope: email:write
Request body — optional
{ "reason": "Customer replied with a new issue." }
The endpoint accepts an optional reason for client-side audit context. The current ticket record stores the reopened status, timestamp, and reopen count; add a thread note if you need the reason retained as ticket history.
GET /v1/alerts/reference/ List notification delivery options  no auth required
200 OK
{ "version": "2026-06-06", "resource": "alerts", "payload": { "create_rule": { "method": "POST", "path": "/v1/alerts/" }, "test_delivery": { "method": "POST", "path": "/v1/alerts/test/" } }, "fields": [...], "delivery_builder": { "type_field": "delivery.type", "target_field": "delivery.target", "fields_by_type": { "webhook": [...], "task": [...] } }, "delivery_types": [ { "type": "webhook", "description": "Send an HTTP notification to a URL.", "required_fields": ["target"], "target_type": "url" } ], "backoff_strategies": [ { "strategy": "exponential", "description": "Retry delay doubles after each failed attempt." } ], "offset_units": ["minutes", "hours", "days"] }
Event type metadata is exposed separately by GET /v1/events/reference/. This endpoint is the builder contract for alert forms and keeps legacy option arrays for compatibility.
GET /v1/messages/blobs/{blob_id}/ Download a blob  scope: files:read
# Binary file content — Content-Type mirrors the uploaded type # Content-Disposition: attachment; filename="download"
DEL /v1/messages/{id}/ Delete a message  scope: email:delete
# 204 No Content — action completed immediately # — or, if the token has requires_approval: ["email:delete"] — # 202 Accepted { "queued": true, "approval_id": "<action-id>" }
GET /v1/messages/folders/ List folders  scope: email:read → role / mailbox_id
[{ "id": "mbox_01...", "name": "Inbox", "role": "inbox", "parentId": null, "totalEmails": 142, "unreadEmails": 3 }]
Pass the role value as ?folder=inbox when listing messages. Use id as mailbox_id for custom folders or PATCH moves. Also available at GET /v1/folders/ (legacy).
GET /v1/folders/ List folders — legacy alias  scope: email:read
Legacy alias for GET /v1/messages/folders/. Returns the same response. Prefer GET /v1/messages/folders/. This path is kept for backward compatibility.
POST /v1/messages/upload/ Upload a file  scope: files:write → blob_id
Request — multipart/form-data, field name: file, max 25 MB
# curl example curl -X POST https://api.gent.mx/v1/messages/upload/ \ -H "Authorization: Bearer gent_..." \ -F "file=@report.pdf"
201 Created
{ "blob_id": "G123abc...", "type": "application/pdf", "size": 204800 }
→ Blob upload field reference
Delivery tracking
  • Permanent delivery failures from recipient mail servers are automatically converted to message.bounced events — no polling required.
  • Subscribe to message.delivered and message.bounced in your webhook config to receive delivery notifications.
  • Use message_id from the send response to correlate delivery events with specific sends.
  • Hard bounces (SMTP 5xx) and spam-policy rejections (5.7.x) automatically suppress the address — future sends skip it.
  • When a recipient marks a message as spam, their address is automatically suppressed. Requires ISP spam feedback registration (Google Postmaster Tools, Yahoo, Outlook).
  • View and manage suppressed addresses via GET /v1/messages/suppressed/.
PATCH /v1/messages/{id}/ Update a message  scope: email:write
Request — all fields optional
{ "mark_read": true, "mark_starred": true, "mailbox_id": "<mailbox-id>" }
FieldTypeRequiredDescription
mark_readbooleanoptionalOptional bool — mark read/unread.
mark_starredbooleanoptionalOptional bool — star/unstar.
mailbox_idstringoptionalMove to mailbox.
204 No Content
# Empty body — success indicated by 204 status code.
→ Message update field reference
from is always set to the inbox address — it cannot be overridden. List endpoints return up to 50 results by default.
Attachments
  1. Upload the file via POST /v1/messages/upload/ — returns a blob_id.
  2. Pass blob_id in the attachments array when sending. Filename and MIME type are resolved automatically.
  3. Blob metadata expires after 7 days — attach within that window or re-upload.
  4. Unattached message uploads are garbage-collected automatically; there is no explicit delete endpoint.
POST /v1/messages/{id}/suggested-labels/{label_id}/ Confirm a label suggestion  scope: labels:write
# Applies the label to the message and removes the suggestion. # 204 No Content — empty body
DEL /v1/messages/{id}/suggested-labels/{label_id}/ Dismiss a label suggestion  scope: labels:write
# Discards the suggestion without applying the label. # 204 No Content — empty body
POST /v1/messages/{id}/suggested-events/ Confirm an event suggestion  scope: calendar:write
Request
{ "calendar_id": "<calendar-id>" }
FieldTypeRequiredDescription
calendar_idstringoptionalCalendar to create the event in.
201 Created — the created calendar event
{ "event_id": "<event-id>", "calendar_id": "<calendar-id>", "title": "Team standup", "start": "2024-06-03T10:00:00", }
DEL /v1/messages/{id}/suggested-events/ Dismiss an event suggestion  scope: calendar:write
# Discards the suggestion without creating an event. # 204 No Content — empty body