Messages
Send, list, get, and cancel channel-native WhatsApp and Instagram messages.
Messaging capabilities lists which WhatsApp message types you can send and receive today and which are not supported yet.
Quoted WhatsApp replies
Add top-level reply_to: { message_id: "Tyxter message ID" } to any WhatsApp message type. The target must be in the same environment, business number and customer conversation. Unknown or foreign targets return message_not_found; targets without a provider ID return reply_target_unavailable. Normal consent, service-window and billing rules apply. Instagram quoted replies are not supported.
Inbound quote/forward receipts expose content.context inmessage.received, and context on detail andinclude=payload list reads. An unresolved quote has a null Tyxter message ID.
Send a message
WhatsApp reactions use message.type: reaction with reaction: { message_id: "Tyxter inbound ID", emoji: "👍" }. An empty emoji removes the reaction. Use the same sender and recipient as the conversation; the target must be at most 30 days old and the 24-hour service window must be open. Reactions are billed normally and stop at sent, with no delivered/read receipt. Target errors are message_not_found (404), reaction_target_invalid and reaction_target_expired (400), or reaction_target_unavailable (409).
Request body
| Field | Type | Notes |
|---|---|---|
| No fields. | ||
Response
| Field | Type | Notes |
|---|---|---|
idrequired | string | |
objectrequired | "message" | |
statusrequired | string | |
status_reasonrequired | string | null | Why this message is in its current status, when there is a reason worth naming. The only value emitted today is "messaging_limit_pacing": the message is queued because the sending number has used up WhatsApp's daily allowance of NEW recipients, so Tyxter is holding the send rather than pushing it into a rejection. Nothing is lost and no action is needed — the message sends by itself once allowance frees up, and re-sending it would only duplicate the message. Null otherwise, including for a message queued for any other reason, so null on its own never means "not paced" — read it together with status. Treat the set of values as open: more reasons may be named later without notice. |
channelrequired | "whatsapp" | "instagram" | "whatsapp_channel" | |
environmentrequired | "sandbox" | "production" | |
template_idrequired | string | null | |
template_version_idrequired | string | null | |
template_versionrequired | integer | null | |
created_atrequired | string<ISO-8601> | pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ |
trace_idrequired | string |
Tyxter guards
Mandatory Tyxter guards apply to marketing in sandbox and production. Identical template name, language, parameters and media content remain blocked until an inbound message after the previous attempt; there is no timed duplicate expiry within retained message history. Reuploading identical bytes under a new asset ID does not change content. A read receipt is not engagement. Different content on a healthy sender is allowed. A seven-day sender/recipient cooldown applies only when phone or template quality is YELLOW/RED or a policy warning occurred within seven days. An inbound message after the latest marketing attempt exempts duplication and that cooldown, even after the 24-hour service window closes. General opt-outs and Meta marketing stop preferences take priority; only a Meta marketing resume clears that provider preference. Meta 131049 requires at least 24 hours before retrying even after engagement; two consecutive 131026 refusals or a latest 130472 holdout require new inbound engagement. Utility and authentication are outside these marketing guards; general consent still applies. HTTP 400 recipient_duplicate_message or recipient_frequency_capped includes error.details.guard_reason, error.details.next_allowed_at and error.feedback. next_allowed_at is null for pending sends or recovery requiring engagement or consent; never schedule a retry from null. Missing or unreadable policy, unclassifiable pending templates, or new marketing media without verified hashes returns 503 recipient_cadence_policy_unavailable. Legacy media equality without a retained hash remains unknown and is logged; no migration invents historic media equality. Queued templates are checked again before dispatch. Batch failure exports expose guard_reason and next_allowed_at when the failure receipt is retained. Recorded provider rejections count as attempts. Guard skips create no provider attempt or message-processing charge and do not renew the cooldown. Customers cannot disable or shorten these protections. They do not guarantee delivery or improved sender quality.
Template sends use message.type = "template". Set message.template.language to a string WhatsApp language code such as pt_BR; do not send a nested language object.
Before a WhatsApp template send is accepted, Tyxter resolves its approved version and checks modeled BODY and text-HEADER placeholders, media headers, and dynamic URL-button parameters. The send request has no parameter_format: the immutable approvedTemplateVersion chooses it. POSITIONAL BODY variables use exact numeric keys such as "1"; NAMED BODY variables use exact approved parameter-name keys such as "customer_name". A named textHEADER or dynamic URL button uses an explicit text parameter carrying the matching parameter_name. A mismatch returns 400 template_param_mismatch with a corrective param such as message.template.variables, message.template.components, or message.template.header_media. The request did not create a message, consume billing, enqueue work, or call Meta. This modeled preflight complements, rather than replaces, the asynchronous failed-message fallback for a later approval change or an unmodeled Meta rule.
WhatsApp recipients accept the compatible recipient.id = "+E.164" form or the additive structured form with country_calling_code and national_number. Structured inputs are checked against the international numbering plan and can return invalid_phone_country_calling_code, invalid_phone_national_number, or invalid_phone_e164. Legacy id accepts 8–15 ASCII digits after an optional leading + and ASCII spaces (U+0020), parentheses, dots, or hyphens are removed. Malformed legacy input returns invalid_phone_e164 with param: recipient.id before Tyxter creates a message or idempotency record.
{
"channel": "whatsapp",
"sender": { "type": "whatsapp_phone_number", "id": "pn_..." },
"recipient": {
"type": "phone_e164",
"country_calling_code": "55",
"national_number": "11903244174"
},
"message": { "type": "text", "text": { "body": "Olá" } }
}{
"channel": "whatsapp",
"sender": { "type": "whatsapp_phone_number", "id": "pn_..." },
"recipient": { "type": "phone_e164", "id": "+5511999999999" },
"message": {
"type": "template",
"template": {
"name": "order_tracking_update",
"language": "pt_BR",
"variables": { "customer_name": "Ana", "order_id": "ORD-123" },
"components": [
{
"type": "header",
"parameters": [
{ "type": "text", "parameter_name": "order_id", "text": "ORD-123" }
]
},
{
"type": "button",
"sub_type": "url",
"index": 0,
"parameters": [
{ "type": "text", "parameter_name": "order_id", "text": "ORD-123" }
]
}
]
}
}
}{
"channel": "whatsapp",
"sender": { "type": "whatsapp_phone_number", "id": "pn_..." },
"recipient": { "type": "phone_e164", "id": "+5511999999999" },
"message": {
"type": "template",
"template": {
"name": "order_update",
"language": "pt_BR",
"variables": { "1": "Ana" }
}
}
}WhatsApp media accepts image, document, audio,video, and sticker. For WhatsApp audio, set message.media.voice to true to request voice-note rendering. Omit it or use false for ordinary audio. Meta requires OGG/Opus mono audio for a voice note. When Tyxter knows the resolved bytes' MIME type, it requires the normalized base type audio/ogg before provider I/O; it does not inspect codecs or channels, and does not convert media. If that known MIME type is not audio/ogg, delivery fails with media_voice_requires_ogg_opus before a provider call. Use OGG/Opus mono audio or omit message.media.voice. Provide exactly one of media.id, media.link, media.asset_id, media.inline, or media.source. Use media.asset_id for single-use or library uploads created through the Media API. A media.source with type = "tts" accepts immediately, renders caller-supplied text into an audio attachment on the media-render worker, and then delivers through the normal media path; it does not invoke an agent workflow or generate the reply text.
{
"channel": "whatsapp",
"sender": { "type": "whatsapp_phone_number", "id": "pn_..." },
"recipient": { "type": "phone_e164", "id": "+5511999999999" },
"message": {
"type": "media",
"media": {
"kind": "audio",
"link": "https://cdn.example.com/voice.ogg",
"voice": true
}
}
}{
"channel": "whatsapp",
"sender": { "type": "whatsapp_phone_number", "id": "pn_..." },
"recipient": { "type": "phone_e164", "id": "+5511999999999" },
"message": {
"type": "media",
"media": {
"kind": "image",
"asset_id": "mda_..."
}
}
}{
"channel": "whatsapp",
"sender": { "type": "whatsapp_phone_number", "id": "pn_..." },
"recipient": { "type": "phone_e164", "id": "+5511999999999" },
"message": {
"type": "media",
"media": {
"kind": "audio",
"source": {
"type": "tts",
"provider": "openai",
"text": "Your order is out for delivery.",
"voice": "marin",
"language": "en-US"
}
}
}
}Direct quick replies are WhatsApp interactive button messages. They require one to three reply buttons and must be sent inside the customer-service window.
{
"channel": "whatsapp",
"sender": { "type": "whatsapp_phone_number", "id": "pn_..." },
"recipient": { "type": "phone_e164", "id": "+5511999999999" },
"message": {
"type": "interactive",
"interactive": {
"type": "button",
"body": { "text": "Confirm delivery?" },
"action": {
"buttons": [
{ "type": "reply", "reply": { "id": "confirm", "title": "Confirm" } }
]
}
}
}
}Native Brazil Pix uses the strict interactive.order_details subtype. Send it only for a WhatsApp Business Account eligible for Meta Payments API in Brazil. As a free-form interactive message, it requires an open 24-hour customer-service window; otherwise Tyxter rejects synchronously with service_window_required. Tyxter returns 202 Accepted only after contract, authentication, feature, and service-window prevalidation. Meta then checks WABA and message eligibility at provider-send time, so acceptance does not promise delivery by Meta.
{
"channel": "whatsapp",
"sender": { "type": "whatsapp_phone_number", "id": "phone_number_id" },
"recipient": { "type": "phone_e164", "id": "+5511999999999" },
"message": {
"type": "interactive",
"interactive": {
"type": "order_details",
"body": { "text": "Review and pay your order." },
"action": {
"name": "review_and_pay",
"parameters": {
"reference_id": "ord_123-20260807",
"type": "digital-goods",
"payment_type": "br",
"payment_settings": [{
"type": "pix_dynamic_code",
"pix_dynamic_code": {
"code": "00020101021226700014br.gov.bcb.pix...",
"merchant_name": "Example Store",
"key": "39580525000189",
"key_type": "CNPJ"
}
}],
"currency": "BRL",
"total_amount": { "value": 12900, "offset": 100 }
}
}
}
}
}The caller supplies the reference, dynamic Pix code, merchant name, key, and key type. Tyxter does not resolve a payment_id into this message and does not expose an order_status update. Meta transports the details but does not reconcile settlement; confirm settlement with the merchant or PSP. Most integrations should collect Pix with a Tyxter payment request instead — the Pix payments recipe documents the recommended transparent Abacate Pix path alongside this native message.
Send a published WhatsApp Flow with message.type = "flow". Address the flow with flow.action.parameters.flow_id (the published flow's provider_flow_id, read from GET /v1/flows/{id}) or with flow.action.parameters.flow_name — provide exactly one. flow_token is your required opaque correlation token and comes back as submission.provider_token on the flow.completed webhook; flow_cta is the required button label (max 30 chars). See the Flows guide for the full create → publish → send → complete loop.
{
"channel": "whatsapp",
"sender": { "type": "whatsapp_phone_number", "id": "pn_..." },
"recipient": { "type": "phone_e164", "id": "+5511999999999" },
"message": {
"type": "flow",
"flow": {
"body": { "text": "Tap below to complete your booking." },
"action": {
"name": "flow",
"parameters": {
"flow_id": "1234567890",
"flow_token": "booking-7f3a",
"flow_cta": "Book now"
}
}
}
}
}Retrieve a message
Response
| Field | Type | Notes |
|---|---|---|
idrequired | string | |
objectrequired | "message" | |
channelrequired | "whatsapp" | "instagram" | "whatsapp_channel" | |
directionrequired | string | |
typerequired | string | |
statusrequired | string | |
status_reasonrequired | string | null | Why this message is in its current status, when there is a reason worth naming. The only value emitted today is "messaging_limit_pacing": the message is queued because the sending number has used up WhatsApp's daily allowance of NEW recipients, so Tyxter is holding the send rather than pushing it into a rejection. Nothing is lost and no action is needed — the message sends by itself once allowance frees up, and re-sending it would only duplicate the message. Null otherwise, including for a message queued for any other reason, so null on its own never means "not paced" — read it together with status. Treat the set of values as open: more reasons may be named later without notice. |
environmentrequired | "sandbox" | "production" | |
senderrequired | object | object | Sender on message list and retrieve responses. profile_name is the captured customer WhatsApp profile name for a resolved inbound WhatsApp sender, otherwise null; it is not a verified identity or business display name. The phone-less inbound WhatsApp fallback is the only arm with an empty id; outbound request identities remain non-empty. |
recipientrequired | object | |
providerrequired | string | null | |
provider_message_idrequired | string | null | |
template_namerequired | string | null | |
template_idrequired | string | null | |
template_version_idrequired | string | null | |
template_versionrequired | integer | null | |
mediarequired | object | object | object | object | null | |
unsupportedrequired | object | null | Set when the provider refused to deliver this inbound message (type "unsupported"). The content never reaches Tyxter; ask the sender to resend in a supported format. |
unknownrequired | object | null | Set when the provider delivered an inbound type Tyxter does not project yet (type "unknown"). The content is in the raw payload; only the typed view is missing. |
context | object | null | Quote/forward context. Present on detail and include=payload reads; omitted on default lists. |
payloadrequired | unknown | null | Raw provider/request payload for diagnostics. Provider-specific fields, including private short-lived media URLs, are unsupported integration handles. Use the first-class media.asset_id descriptor for inbound attachments. |
metadatarequired | unknown | null | |
error_coderequired | string | null | |
error_messagerequired | string | null | |
provider_errorrequired | object | null | |
trace_idrequired | string | |
created_atrequired | string<ISO-8601> | pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ |
updated_atrequired | string<ISO-8601> | pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ |
redacted_atrequired | string<ISO-8601> | null | |
delivery_unconfirmed_atrequired | string<ISO-8601> | null | |
eventsrequired | object[] |
Inbound attachments expose a first-class media descriptor on this response: its asset_id is the Tyxter mda_* handle accepted by GET /v1/media/{asset_id}/download-url and by the message-bound transcription flow. The same descriptor appears on list rows and on message.received, with one status switch across all three surfaces. When status is consumed, media.download contains the relative GET method/path for that authenticated download hop, never a signed URL.
The detail response also includes payload as a raw provider/request diagnostic window. Provider-specific fields are unsupported integration contracts. In particular, a Meta lookaside.fbsbx.com URL is private, short-lived, and bearer-authenticated; do not render or fetch it. Follow media.download.path and mint a fresh Tyxter download capability instead.
Every read sender has profile_name. It is the captured WhatsApp profile name for a resolved inbound WhatsApp sender and null for outbound, non-WhatsApp, phone-less, historical rows without captured evidence, or redacted messages. It is not a verified identity or a business display name, and Tyxter does not store it as a contact field.
A message that stays queued longer than you expected may be paced rather than stuck. status_reason names the cause when there is one worth naming: today the only value is messaging_limit_pacing, meaning the sender has no slot available for this unique recipient outside their customer-service window. It is not a total-message cap: recipients with an existing rolling slot and recipients who messaged this sender in the last 24 hours keep flowing. Nothing is lost — the message sends by itself once allowance frees up, and re-sending it would only duplicate it. A null here does not mean “not paced” on its own; read it together with status. Treat the set of values as open, check remaining_messaging_allowance_estimate on the sending number, and read the pacing guide for the exact counting rules.
List messages
Use created_after=2026-10-11T10:00:00.000Z to return only messages created strictly after an ISO 8601 timestamp with a timezone. The SDK accepts client.messages.list({ created_after, starting_after, limit }). Keep this lower bound and other filters fixed while following next_cursor as starting_after until has_more is false. Results remain newest first, with message ID breaking timestamp ties. Invalid timestamps return 400 invalid_messages_query, error.param=created_after and error.feedback.
This selects creation time, not status updates. Overlap polling windows, deduplicate by message ID, and finish every page before advancing the watermark. Back off on empty results. Concurrent transactions can commit late, so use signed webhooks or GET /v1/webhook-events/listen for reliable events. Filtering reduces response size; switching to events reduces calls.
Query params
| Field | Type | Notes |
|---|---|---|
limitrequired | integer | Maximum number of items to return. Defaults to 20; maximum 100. range 1..100 |
starting_after | string | Opaque cursor from the previous response next_cursor. |
created_after | string<ISO-8601> | Return messages created strictly after this ISO 8601 timestamp. Keep it fixed while paging with starting_after. This does not select status updates. pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ |
status | string | |
batch_id | string | |
direction | "inbound" | "outbound" | Return only messages in this direction: "inbound" or "outbound". |
include | "payload" |
Response
| Field | Type | Notes |
|---|---|---|
objectrequired | "list" | |
datarequired | object[] | |
has_morerequired | boolean | |
next_cursorrequired | string | null |
List responses omit large message bodies by default: each item returns payload: null and metadata: null. Pass include=payload when an inspector or agent needs both fields. The first-class media descriptor is not part of that expansion: it is present by default for a settled inbound attachment so a polling inbox never needs to inspect provider payloads or match against the media list. For inbound WhatsApp audio, optional media.voice is true for a Meta voice note and false for an ordinary audio file; omission means a legacy row or absent provider signal.
A payload-less list row returns sender.profile_name: null; with include=payload, an inbound WhatsApp row can return its captured profile name. Retrieve responses read the stored evidence directly. Privacy cleanup removes that evidence, after which new reads return null.
Pass direction=inbound (or outbound) to filter server-side. An inbox-style reader should use it rather than over-fetching and filtering locally: on a busy account, outbound traffic can otherwise fill the whole first page.
Phone-like sender.id and recipient.id values are normally returned in strict +E.164 with every subscriber digit preserved. One output-only case is intentionally unresolved: when Meta withholds the customer phone on a production inbound WhatsApp message, reads return { type: "phone_e164", id: "" } for its sender. Do not use that empty id as a recipient or replace it with a synthetic identifier. Recipients and outbound request identities remain non-empty. Tyxter may maintain separate internal identity evidence, but list and retrieve responses never substitute it for the public identity. Opaque Meta phone-number ids remain unchanged.
const page = await tyxter.messages.list({
status: 'failed',
include: 'payload',
});
// page.data[0].media is always the supported inbound attachment handle.
// page.data[0].payload and .metadata now also contain raw diagnostic data.Cancel a message
Response
| Field | Type | Notes |
|---|---|---|
idrequired | string | |
objectrequired | "message" | |
channelrequired | "whatsapp" | "instagram" | "whatsapp_channel" | |
directionrequired | string | |
typerequired | string | |
statusrequired | string | |
status_reasonrequired | string | null | Why this message is in its current status, when there is a reason worth naming. The only value emitted today is "messaging_limit_pacing": the message is queued because the sending number has used up WhatsApp's daily allowance of NEW recipients, so Tyxter is holding the send rather than pushing it into a rejection. Nothing is lost and no action is needed — the message sends by itself once allowance frees up, and re-sending it would only duplicate the message. Null otherwise, including for a message queued for any other reason, so null on its own never means "not paced" — read it together with status. Treat the set of values as open: more reasons may be named later without notice. |
environmentrequired | "sandbox" | "production" | |
senderrequired | object | object | Sender on message list and retrieve responses. profile_name is the captured customer WhatsApp profile name for a resolved inbound WhatsApp sender, otherwise null; it is not a verified identity or business display name. The phone-less inbound WhatsApp fallback is the only arm with an empty id; outbound request identities remain non-empty. |
recipientrequired | object | |
providerrequired | string | null | |
provider_message_idrequired | string | null | |
template_namerequired | string | null | |
template_idrequired | string | null | |
template_version_idrequired | string | null | |
template_versionrequired | integer | null | |
mediarequired | object | object | object | object | null | |
unsupportedrequired | object | null | Set when the provider refused to deliver this inbound message (type "unsupported"). The content never reaches Tyxter; ask the sender to resend in a supported format. |
unknownrequired | object | null | Set when the provider delivered an inbound type Tyxter does not project yet (type "unknown"). The content is in the raw payload; only the typed view is missing. |
context | object | null | Quote/forward context. Present on detail and include=payload reads; omitted on default lists. |
payloadrequired | unknown | null | Raw provider/request payload for diagnostics. Provider-specific fields, including private short-lived media URLs, are unsupported integration handles. Use the first-class media.asset_id descriptor for inbound attachments. |
metadatarequired | unknown | null | |
error_coderequired | string | null | |
error_messagerequired | string | null | |
provider_errorrequired | object | null | |
trace_idrequired | string | |
created_atrequired | string<ISO-8601> | pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ |
updated_atrequired | string<ISO-8601> | pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ |
redacted_atrequired | string<ISO-8601> | null | |
delivery_unconfirmed_atrequired | string<ISO-8601> | null | |
eventsrequired | object[] |
Show a typing indicator
Response
| Field | Type | Notes |
|---|---|---|
objectrequired | "typing_indicator" | |
message_idrequired | string | |
statusrequired | "accepted" |
One call does two things that always travel together: it marks the inbound message as read and it shows the typing indicator on that conversation. There is no read-only and no typing-only variant. WhatsApp draws the indicator for at most 25 seconds and dismisses it the moment your reply is delivered, so call this immediately before you start composing — not on receipt — and send the reply inside that window. If composing will take longer than 25 seconds, send an interim reply rather than leaving the contact waiting on an indicator that has already lapsed. It is never billed and never appears on your usage.
Take the target from message.received.data.message_id. That is the Tyxter message ID this route accepts. The webhook envelope’s top-level id is an event ID, and data.provider_message_id is Meta’s reference; neither is a valid substitute. In untyped JavaScript, check that data.message_id is a non-empty string before calling the SDK. Reading a missing property can otherwise produce the literal path /v1/messages/undefined/typing, which returns message_not_found and shows no indicator.
The call creates no message: nothing appears in GET /v1/messages, no webhook fires, and there is no delivery status to poll. The 202 is the only acknowledgement, and a provider-side failure after it is not reported back. The target is validated before acceptance, so branch on the response: the id must name an inbound message you received (typing_indicator_target_not_inbound, 400), on a channel whose provider supports the indicator — today only WhatsApp (typing_indicator_unsupported_channel, 400) — received within the last 30 days (typing_indicator_target_expired, 400). An unknown id, or one belonging to another organization, answers message_not_found (404). A valid target that carries no provider reference to draw against answers typing_indicator_target_unavailable (409); retrying will not clear it, so answer with a normal reply instead. The receiving phone must have current service authority: phone_service_suspended (403) means correct its funding or connection state; sender_phone_number_unavailable (400) means it cannot be resolved to an available sender. These checks also apply to managed sandbox phones. An unbound simulated recipient remains supported. Service is checked again before dispatch; a later refusal drops the indicator.
The endpoint takes no Idempotency-Key. Each accepted request independently asks to show the indicator, including after an earlier indicator expires. It needs the same messages:send scope a reply needs. Supply Tyxter-Trace-Id to correlate the acknowledgement with the asynchronous job and worker logs. Sandbox keys are accepted and the indicator is simulated (no real device shows anything), so the flow can be rehearsed end to end before going live.
async function showTypingForInbound(event: {
type?: unknown;
data?: { message_id?: unknown };
}) {
if (event.type !== 'message.received') return;
const messageId = event.data?.message_id;
if (typeof messageId !== 'string' || messageId.trim() === '') {
throw new Error('message.received is missing data.message_id');
}
await tyxter.messages.typing(messageId, { traceId: 'trc_reply_123' });
// Generate and send the reply with tyxter.whatsapp.sendText(...) next.
}Request an inbound audio transcription
Request body
| Field | Type | Notes |
|---|---|---|
language | string | Optional ISO-639-1 language hint, for example pt or en. pattern: ^[a-z]{2}$ |
prompt | string | Optional advisory context, trimmed; maximum 1024 UTF-16 code units. Empty clears context. Omission inherits on retry. Never returned as a request field. len 0..1024 |
keywords | string[] | Optional advisory vocabulary: at most 50 trimmed strings of 1–128 UTF-16 code units; no CR, LF, U+2028, U+2029, < or >. Order, case and duplicates are preserved. Empty array clears vocabulary; omission inherits on retry. Never returned as a request field. |
Response
| Field | Type | Notes |
|---|---|---|
idrequired | string | |
objectrequired | "message_media_transcript" | |
message_idrequired | string | |
media_asset_idrequired | string | |
statusrequired | "pending" | "succeeded" | "failed" | |
providerrequired | string | null | |
modelrequired | string | null | |
languagerequired | string | null | |
textrequired | string | null | |
duration_secondsrequired | number | null | |
error_coderequired | string | null | |
error_messagerequired | string | null | |
trace_idrequired | string | |
created_atrequired | string<ISO-8601> | pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ |
completed_atrequired | string<ISO-8601> | null |
This is an explicit per-message opt-in and accepts only inbound audio whose Tyxter media asset is ready for playback. A pending or succeeded receipt replays when supplied language, prompt and keywords match the newest generation after normalization; omitted fields replay unchanged. A failed receipt answers transcription_retry_required; this create operation never starts another provider attempt. Transcription runs asynchronously and never delays message.received. phone_service_suspended denies newly starting paid work when current inbound phone-service authority is unavailable; a completed receipt or accepted replay keeps its existing result.
To avoid polling, subscribe to both terminal events: message.media_transcribed for success and message.media_transcription_failed for failure. The failed event carries the same error_code as this route's GET receipt and never carries speech, provider, model, or duration fields.
Optional prompt supplies advisory context: trimmed, at most 1024 UTF-16 code units. Optional keywords supplies up to 50 trimmed terms of 1–128 UTF-16 code units each, preserving order, case and duplicates. Terms cannot contain CR, LF, U+2028, U+2029 or angle brackets. Invalid fields return invalid_transcription_request; differing create hints return transcription_hints_conflict. Request hints are not returned as fields in transcript responses or webhooks. They follow message retention and contact erasure.
Retry a failed inbound audio transcription
Request body
| Field | Type | Notes |
|---|---|---|
language | string | Optional ISO-639-1 language hint, for example pt or en. pattern: ^[a-z]{2}$ |
prompt | string | Optional advisory context, trimmed; maximum 1024 UTF-16 code units. Empty clears context. Omission inherits on retry. Never returned as a request field. len 0..1024 |
keywords | string[] | Optional advisory vocabulary: at most 50 trimmed strings of 1–128 UTF-16 code units; no CR, LF, U+2028, U+2029, < or >. Order, case and duplicates are preserved. Empty array clears vocabulary; omission inherits on retry. Never returned as a request field. |
Response
| Field | Type | Notes |
|---|---|---|
idrequired | string | |
objectrequired | "message_media_transcript" | |
message_idrequired | string | |
media_asset_idrequired | string | |
statusrequired | "pending" | "succeeded" | "failed" | |
providerrequired | string | null | |
modelrequired | string | null | |
languagerequired | string | null | |
textrequired | string | null | |
duration_secondsrequired | number | null | |
error_coderequired | string | null | |
error_messagerequired | string | null | |
trace_idrequired | string | |
created_atrequired | string<ISO-8601> | pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ |
completed_atrequired | string<ISO-8601> | null |
This route reopens only a failed receipt against its exact original inbound audio. It requires a non-blank Idempotency-Key: reuse the same key and body to replay the stored202, rather than creating another generation. The receipt id and original asset stay stable. Omitted prompt and keywords inherit from the newest generation; an empty prompt or empty array clears the corresponding field. Same-key comparison uses supplied, normalized fields, so omission and explicit clearing are distinct intents. media_asset_expired means that otherwise-valid original audio is beyond retention; transcription_source_unavailable means the original source is structurally no longer usable. Neither can be repaired by uploading a replacement attachment. A fresh-key retry can return phone_service_suspended before its provider attempt starts; accepted replay remains unchanged.
Retrieve an inbound audio transcription
Response
| Field | Type | Notes |
|---|---|---|
idrequired | string | |
objectrequired | "message_media_transcript" | |
message_idrequired | string | |
media_asset_idrequired | string | |
statusrequired | "pending" | "succeeded" | "failed" | |
providerrequired | string | null | |
modelrequired | string | null | |
languagerequired | string | null | |
textrequired | string | null | |
duration_secondsrequired | number | null | |
error_coderequired | string | null | |
error_messagerequired | string | null | |
trace_idrequired | string | |
created_atrequired | string<ISO-8601> | pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ |
completed_atrequired | string<ISO-8601> | null |