Edit and delete messages
Both operations act on a message you already sent. The message_id is the same msg_{hex32} returned by the original send or seen in webhooks/polling. Both return 202 Accepted immediately; the actual result arrives asynchronously via webhook.
Use POST /v1/messages/{id}/edit and POST /v1/messages/{id}/delete for the main number. For extra numbers, use POST /v1/numbers/{ref}/messages/{id}/edit and .../delete.
Required scope: messages:send
Before editing or deleting, Turbo Notify opens the conversation the way a person would: the number shows as online, the unread messages in the chat the target message belongs to are marked as read, and only then does the edit or deletion happen. See Mark as read.
What can be changed after sending
| Message | Edit | Delete for everyone |
|---|---|---|
| Text | Yes. Send the new text. | Yes |
| Image, video or document | Yes, the caption only. Send the new caption. | Yes |
| Calendar event | Yes, the whole event is replaced, or the event is canceled. | Yes |
| Location, contact card, link button, sticker or audio | No | Yes, the only change |
The file of an image, video or document never changes. To send a different file, send a new message. Only messages sent through the API by this number can be changed (origin: "tn").
A text or a caption can only be changed within 15 minutes of sending it, which is WhatsApp’s own limit. After that the API answers 400 edit_window_expired, and the way out is to send a new message. The 15 minutes do not apply to a calendar event, although WhatsApp may still refuse an event edit (the outcome arrives as message.edit.failed).
Edit a message
Replaces the text of a text message you already sent (native WhatsApp editing: the conversation history shows “edited”). To change the caption of an image, video or document, see the caption section. To edit or cancel a calendar event, see the event section.
curl -X POST \ https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages/msg_01993c5a1b24d3e9f01a2b3c4d5e6f70/edit \ -H "Authorization: Bearer YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "Corrected message text." }'const messageId = 'msg_01993c5a1b24d3e9f01a2b3c4d5e6f70';const response = await fetch( `https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages/${messageId}/edit`, { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ text: 'Corrected message text.' }) });
const result = await response.json();console.log(result);import requests
message_id = "msg_01993c5a1b24d3e9f01a2b3c4d5e6f70"url = f"https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages/{message_id}/edit"headers = { "Authorization": "Bearer YOUR_KEY", "Content-Type": "application/json"}data = {"text": "Corrected message text."}
response = requests.post(url, json=data, headers=headers)result = response.json()print(result)package main
import ( "fmt" "io" "net/http" "strings")
func main() { payload := strings.NewReader(`{"text": "Corrected message text."}`) req, _ := http.NewRequest( "POST", "https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages/msg_01993c5a1b24d3e9f01a2b3c4d5e6f70/edit", payload, ) req.Header.Set("Authorization", "Bearer YOUR_KEY") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) fmt.Println(string(body))}<?php$messageId = 'msg_01993c5a1b24d3e9f01a2b3c4d5e6f70';$url = "https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages/{$messageId}/edit";$data = ['text' => 'Corrected message text.'];
$options = [ 'http' => [ 'header' => [ 'Authorization: Bearer YOUR_KEY', 'Content-Type: application/json' ], 'method' => 'POST', 'content' => json_encode($data) ]];
$context = stream_context_create($options);$result = file_get_contents($url, false, $context);$response = json_decode($result, true);var_dump($response);?>using System.Text;using System.Text.Json;
var client = new HttpClient();client.DefaultRequestHeaders.Add("Authorization", "Bearer YOUR_KEY");
var messageId = "msg_01993c5a1b24d3e9f01a2b3c4d5e6f70";var data = new { text = "Corrected message text." };var json = JsonSerializer.Serialize(data);var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await client.PostAsync( $"https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages/{messageId}/edit", content);var result = await response.Content.ReadAsStringAsync();Console.WriteLine(result);import java.net.http.*;import java.net.URI;
public class EditMessage { public static void main(String[] args) throws Exception { var client = HttpClient.newHttpClient(); var messageId = "msg_01993c5a1b24d3e9f01a2b3c4d5e6f70";
var json = """ {"text": "Corrected message text."} """;
var request = HttpRequest.newBuilder() .uri(URI.create( "https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages/" + messageId + "/edit")) .header("Authorization", "Bearer YOUR_KEY") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(json)) .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); }}Request body
| Field | Type | Required | Notes |
|---|---|---|---|
text | string | yes | 1 to 4,096 characters. The only accepted field: the body is strict (any other field returns 422). |
Response (202)
{
"message_id": "msg_01993c5a1b24d3e9f01a2b3c4d5e6f70",
"status": {
"state": "pending",
"at": "2026-06-01T13:05:01Z",
"error": null
}
} The actual result arrives via the message.edited webhook (success) or
message.edit.failed (failure, with an error object). After the edit,
GET /v1/numbers/{ref}/messages/{id} already returns the updated text.
Mentions in edits
The new text of the edit (and the new caption, see Edit a caption) also goes through @<number> mention detection,
with the same rules as Mentions (@mentions).
The edit replaces the message’s whole set of mentions: the mentions
returned by GET /v1/numbers/{ref}/messages/{id} (and the data.mentions of the
message.edited webhook) then reflects only the tokens present in the new
text. An edit that removes every @<number> from the original text makes
mentions disappear from the response.
Edit a caption
Replaces the caption of an image, video or document your number sent. Use the same edit route and send caption instead of text. The recipient sees the new caption marked as “edited”. The file stays as it was and the message keeps its id.
curl -X POST \ https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages/msg_01993c5a1b24d3e9f01a2b3c4d5e6f70/edit \ -H "Authorization: Bearer YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "caption": "Invoice for October, corrected total." }'const messageId = 'msg_01993c5a1b24d3e9f01a2b3c4d5e6f70';const response = await fetch( `https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages/${messageId}/edit`, { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ caption: 'Invoice for October, corrected total.' }) });
console.log(await response.json());import requests
message_id = "msg_01993c5a1b24d3e9f01a2b3c4d5e6f70"response = requests.post( f"https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages/{message_id}/edit", headers={ "Authorization": "Bearer YOUR_KEY", "Content-Type": "application/json", }, json={"caption": "Invoice for October, corrected total."},)print(response.json())Request body (caption)
| Field | Type | Required | Notes |
|---|---|---|---|
caption | string | yes | 1 to 1,024 characters. It replaces the whole caption and cannot be empty. Accepts the same @<number> mentions as the caption of a send. The only accepted field: any other field returns 422. |
Response and outcome
The response is the same as for any edit: 202 with the message_id and the pending status. The outcome arrives by webhook (or by polling):
message.editedwhen the caption was changed. It carries thecontent_typeof the message (image,videoordocument) and the new caption inpreview(up to 50 characters, as with text).message.edit.failedwhen the edit was not made.
Captions edited in the WhatsApp app also arrive as message.edited, with origin: "native", the content_type of the file and the new caption in preview.
The edit does not consume the message quota. A caption body on a text message, a text body on an image, or any body on a message that cannot be edited answers 400 message_not_editable.
Edit or cancel a calendar event
A calendar event your number sent (see Send a calendar event) can also be edited or canceled. Use the same edit route and send the event body instead of text. The name field tells the API the body is an event.
The edit replaces the whole event: whatever you do not send is removed from it. That is why name and start_at are required, as when sending, and why an edit without location removes the place.
curl -X POST \ https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages/msg_01993c5a1b24d3e9f01a2b3c4d5e6f70/edit \ -H "Authorization: Bearer YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Sprint Planning", "description": "Planning for sprint 24. New room.", "start_at": "2026-11-05T15:00:00Z", "end_at": "2026-11-05T16:00:00Z", "join_link": "https://meet.example.com/abc", "location": { "latitude": -23.5505, "longitude": -46.6333, "name": "HQ - Blue Room" } }'const messageId = 'msg_01993c5a1b24d3e9f01a2b3c4d5e6f70';const response = await fetch( `https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages/${messageId}/edit`, { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Sprint Planning', description: 'Planning for sprint 24. New room.', start_at: '2026-11-05T15:00:00Z', end_at: '2026-11-05T16:00:00Z', join_link: 'https://meet.example.com/abc', location: { latitude: -23.5505, longitude: -46.6333, name: 'HQ - Blue Room' } }) });
console.log(await response.json());import requests
message_id = "msg_01993c5a1b24d3e9f01a2b3c4d5e6f70"response = requests.post( f"https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages/{message_id}/edit", headers={ "Authorization": "Bearer YOUR_KEY", "Content-Type": "application/json", }, json={ "name": "Sprint Planning", "description": "Planning for sprint 24. New room.", "start_at": "2026-11-05T15:00:00Z", "end_at": "2026-11-05T16:00:00Z", "join_link": "https://meet.example.com/abc", "location": { "latitude": -23.5505, "longitude": -46.6333, "name": "HQ - Blue Room", }, },)print(response.json())Request body (event)
The fields are the same as in sending a calendar event, without the content object, plus canceled.
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | event title; up to 200 characters (the field that marks the body as an event) |
start_at | datetime | yes | ISO-8601 with time zone |
end_at | datetime | no | ISO-8601 with time zone; must be > start_at |
description | string | no | up to 2000 characters |
location | object | no | same format as the location object of a send |
join_link | string | no | http:///https:// only |
has_reminder | bool | no | defaults to false |
reminder_offset_sec | int | no | seconds before the event; only takes effect with has_reminder=true |
extra_guests_allowed | bool | no | defaults to false |
canceled | bool | no | defaults to false. With true, the event is canceled. |
Cancel the event
Send "canceled": true in the same body. The event stays in the chat, shows as canceled, and WhatsApp notifies the guests.
curl -X POST \ https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages/msg_01993c5a1b24d3e9f01a2b3c4d5e6f70/edit \ -H "Authorization: Bearer YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Sprint Planning", "start_at": "2026-11-05T15:00:00Z", "canceled": true }'Response and outcome
The response is the same as for any edit: 202 with the message_id and the pending status. The outcome arrives by webhook (or by polling):
message.editedwhen the event was changed. It carriescontent_type: "calendar", the event name inpreview, andcanceled: trueonly when the event was canceled.message.edit.failedwhen the edit was not made. Witherror.codeset tomessage_secret_unavailable, this number can no longer change the event, for example because it was linked again after the event was sent. Send a new event with the new details. Withcalendar_edit_unavailable, the platform cannot change events at the moment and repeating the request does not help: send a new event with the new details. WhatsApp may also refuse an edit after some time, and the outcome is then the samemessage.edit.failed.
Examples of each event are in Calendar events.
Rules
- Only an event sent by this number can be edited, and only in status
sent,deliveredorread. - A body that does not fit the message (text on a calendar event, an event on a text message, a caption on a text) answers
400 message_not_editable. - A message from another number answers
404 not_found. - The edit does not consume the message quota.
What happens to the guests
| Situation | What to do |
|---|---|
| A correction: typo, description or link | Edit the event. The card shows the change and a guest who already answered keeps the answer. |
| The date, time or location changed and guests must accept the new conditions | Cancel the event and send a new one. The new message draws the guest’s attention, and the old answer (“Going”) does not carry over. |
Neither an edit nor a cancel changes the entry a guest created in their own calendar with the “Add to calendar” button: it stays on the old date and is not marked as canceled, and adding it again creates a second entry. Ask the guest to delete the old entry.
Delete (revoke) a message
Revokes a message you already sent (the same as “Delete for everyone” on WhatsApp). The request body is empty.
curl -X POST \ https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages/msg_01993c5a1b24d3e9f01a2b3c4d5e6f70/delete \ -H "Authorization: Bearer YOUR_KEY"const messageId = 'msg_01993c5a1b24d3e9f01a2b3c4d5e6f70';const response = await fetch( `https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages/${messageId}/delete`, { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_KEY' } });
const result = await response.json();console.log(result);import requests
message_id = "msg_01993c5a1b24d3e9f01a2b3c4d5e6f70"url = f"https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages/{message_id}/delete"headers = {"Authorization": "Bearer YOUR_KEY"}
response = requests.post(url, headers=headers)result = response.json()print(result)package main
import ( "fmt" "io" "net/http")
func main() { req, _ := http.NewRequest( "POST", "https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages/msg_01993c5a1b24d3e9f01a2b3c4d5e6f70/delete", nil, ) req.Header.Set("Authorization", "Bearer YOUR_KEY") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) fmt.Println(string(body))}<?php$messageId = 'msg_01993c5a1b24d3e9f01a2b3c4d5e6f70';$url = "https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages/{$messageId}/delete";
$options = [ 'http' => [ 'header' => ['Authorization: Bearer YOUR_KEY'], 'method' => 'POST' ]];
$context = stream_context_create($options);$result = file_get_contents($url, false, $context);$response = json_decode($result, true);var_dump($response);?>var client = new HttpClient();client.DefaultRequestHeaders.Add("Authorization", "Bearer YOUR_KEY");
var messageId = "msg_01993c5a1b24d3e9f01a2b3c4d5e6f70";var response = await client.PostAsync( $"https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages/{messageId}/delete", null);var result = await response.Content.ReadAsStringAsync();Console.WriteLine(result);import java.net.http.*;import java.net.URI;
public class DeleteMessage { public static void main(String[] args) throws Exception { var client = HttpClient.newHttpClient(); var messageId = "msg_01993c5a1b24d3e9f01a2b3c4d5e6f70";
var request = HttpRequest.newBuilder() .uri(URI.create( "https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages/" + messageId + "/delete")) .header("Authorization", "Bearer YOUR_KEY") .POST(HttpRequest.BodyPublishers.noBody()) .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); }}Response (202)
{
"message_id": "msg_01993c5a1b24d3e9f01a2b3c4d5e6f70",
"status": {
"state": "pending",
"at": "2026-06-01T13:06:01Z",
"error": null
}
} The actual result arrives via the message.deleted webhook (success) or
message.delete.failed (failure, with an error object).
After a successful deletion, GET /v1/numbers/{ref}/messages/{id} no longer
returns the original content, both for whoever deleted it and for every
recipient of the message. The message’s content_type does not change: what
changes is the content of the matching structured field:
Original content_type | Affected field | Value after deletion |
|---|---|---|
text | text | "" (empty string) |
image / video / audio / document / sticker | image / video / audio / document / sticker | {"kind": "unavailable", "reason": "deleted"}: the media is no longer reachable via signed URL |
location | location | {"kind": "unavailable", "reason": "deleted"} |
calendar | calendar | {"kind": "unavailable", "reason": "deleted"} |
contact | contact | {"kind": "unavailable", "reason": "deleted"} |
interactive | interactive | {"kind": "unavailable", "reason": "deleted"} |
Every type other than text uses the same object, so “this content is no longer
available” has a single format, whatever the content_type.
Possible errors
Both endpoints share the same set of errors:
| Status | Error code | Description |
|---|---|---|
400 | message_not_editable | (edit only) The message was received (sent by someone else), was sent from the phone itself (origin: "native"), is not a text, a calendar event or the caption of an image, video or document, the body does not fit the message (text on an event, an event on a text, a caption on a text), has not been sent yet, was already deleted, or its current status (failed, for example) does not allow editing. Only text messages, calendar events and the caption of an image, video or document can be edited. |
400 | edit_window_expired | (edit only) More than 15 minutes have passed since a text or a caption was sent, the time WhatsApp gives to change them. Send a new message. The 15 minutes do not apply to a calendar event. |
400 | message_not_deletable | (delete only) The message was received (sent by someone else), was sent from the phone itself (origin: "native"), has not been sent yet, was already deleted, or its current status does not allow deletion. |
401 | not_authenticated | The API key is invalid or missing. |
403 | insufficient_scope | The API key does not have the messages:send scope (the response includes required_scope). |
404 | number_not_found | The alias or id in the URL does not match any registered number. |
400 | invalid_number_id | The value in the URL starts with num_ but is not a valid number id. |
404 | not_found | The message was not found, belongs to another organization, or belongs to a number other than the one in the URL (on the shortcut route without {ref}, the main number): all of these cases return the same response, with no distinction. |
404 | request_not_found | The message could no longer be found while the edit or deletion was being processed. Look up the message with GET and, if it still exists, try again. |
409 | number_not_connected | The number that sent the message is not connected to WhatsApp. Reconnect the number and try again. |
500 | internal_error | Unexpected internal server error. |
Which messages can be edited or deleted
The message must meet the criteria below. If it fails any one, the
request is rejected immediately, with 400 message_not_editable /
400 message_not_deletable (or 400 edit_window_expired, for the edit time limit), before any contact with WhatsApp:
- Sent by this number, through this API: the message must have been
sent through the Turbo Notify API by this number (
direction: "outbound"andorigin: "tn"). Two kinds of message are excluded by this criterion:- Received messages: messages a contact or participant sent to you. They belong to the sender, not to you, and WhatsApp does not let you edit or revoke a message you did not send.
- Messages typed on the phone itself (
origin: "native"), sent outside the API, directly on the device. Turbo Notify is not the client that originated that message, so it cannot edit or revoke it either.
- Content type (edit only): WhatsApp only allows editing
text messages, the caption of an image, video or document, and
calendar events, and the request body must match the message:
textfor a text message,captionfor an image, video or document, the event fields (name,start_at) for an event. Editing audio, a sticker or another type (location, contact card, link button) fails with400 message_not_editable: for those, deleting is the only change. - Status: only messages that are already
sent,deliveredorread, and that have not been deleted yet, are accepted. Messages stillpending, alreadyfailed, or already deleted return400 message_not_editable/400 message_not_deletable. - Time limit (text and caption edits only): a text or a caption can only be
changed within 15 minutes of sending it. After that the answer is
400 edit_window_expired. The 15 minutes do not apply to a calendar event.
See also: Get a message · Message status · Webhook.