curl --request PATCH \
--url https://api.blujay.io/v1/messages/{message_id} \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"text": "See you at 4 instead"
}
'const options = {
method: 'PATCH',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({text: 'See you at 4 instead'})
};
fetch('https://api.blujay.io/v1/messages/{message_id}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.blujay.io/v1/messages/{message_id}"
payload = { "text": "See you at 4 instead" }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.patch(url, json=payload, headers=headers)
print(response.text)package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.blujay.io/v1/messages/{message_id}"
payload := strings.NewReader("{\n \"text\": \"See you at 4 instead\"\n}")
req, _ := http.NewRequest("PATCH", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}require 'uri'
require 'net/http'
url = URI("https://api.blujay.io/v1/messages/{message_id}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Patch.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"text\": \"See you at 4 instead\"\n}"
response = http.request(request)
puts response.read_body<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.blujay.io/v1/messages/{message_id}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PATCH",
CURLOPT_POSTFIELDS => json_encode([
'text' => 'See you at 4 instead'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}{
"status": "success",
"trace_id": "trc_8f2a1c0e4b7d",
"timestamp": "2026-10-14T17:01:00.000Z",
"data": {
"message_id": "msg_abc123",
"chat_id": "chat_abc123",
"phone_number_id": "phone_abc123",
"direction": "inbound",
"preferred_service": "iMessage",
"service": "iMessage",
"status": "received",
"parts": [
{
"type": "text",
"text": "Hi Ava, this is Sam from the Northwind support team.",
"text_decorations": [
{
"range": [
1,
1
],
"style": "bold"
}
]
}
],
"degradations": [
{
"code": "effect_not_delivered",
"reason": "The iMessage effect was not delivered because the message was sent as SMS or RCS."
}
],
"reply_to": {
"message_id": "msg_abc123",
"part_index": 0,
"thread_root_message_id": "msg_abc123"
},
"sent_at": "2026-10-14T17:01:02.000Z",
"delivered_at": "2026-10-14T17:01:03.000Z",
"read_at": "2023-11-07T05:31:56Z",
"failed_at": "2023-11-07T05:31:56Z",
"failure": {
"code": "recipient_unavailable",
"reason": "The messaging network rejected the message."
},
"edited_at": "2026-10-14T17:01:00.000Z",
"edit_count": 0,
"unsent_at": "2026-10-14T17:01:00.000Z",
"unsent_scope": "all",
"created_at": "2026-10-14T17:01:00.000Z",
"updated_at": "2026-10-14T17:01:00.000Z",
"contact_id": "contact_abc123",
"sender": {
"type": "phone",
"address": "<string>",
"contact_id": "contact_abc123"
},
"failure_code": "ATTACHMENT_FETCH_FAILED",
"effect": {
"type": "screen",
"name": "confetti"
},
"retryable": true,
"client_reference": "<string>",
"edit_history": [
{
"text": "See you at 3",
"replaced_at": "2026-10-14T17:01:00.000Z"
}
],
"trace_id": "trc_8f2a1c0e4b7d"
},
"client_request_id": "<string>"
}{
"status": "error",
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "<string>",
"type": "validation",
"http_status": 400,
"retryable": true,
"doc_url": "<string>",
"details": [
{}
],
"retry_after": 2,
"reset_at": "2026-10-14T17:01:00.000Z"
},
"trace_id": "trc_8f2a1c0e4b7d",
"timestamp": "2026-10-14T17:01:00.000Z",
"client_request_id": "<string>"
}{
"status": "error",
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "<string>",
"type": "validation",
"http_status": 400,
"retryable": true,
"doc_url": "<string>",
"details": [
{}
],
"retry_after": 2,
"reset_at": "2026-10-14T17:01:00.000Z"
},
"trace_id": "trc_8f2a1c0e4b7d",
"timestamp": "2026-10-14T17:01:00.000Z",
"client_request_id": "<string>"
}{
"status": "error",
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "<string>",
"type": "validation",
"http_status": 400,
"retryable": true,
"doc_url": "<string>",
"details": [
{}
],
"retry_after": 2,
"reset_at": "2026-10-14T17:01:00.000Z"
},
"trace_id": "trc_8f2a1c0e4b7d",
"timestamp": "2026-10-14T17:01:00.000Z",
"client_request_id": "<string>"
}{
"status": "error",
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "<string>",
"type": "validation",
"http_status": 400,
"retryable": true,
"doc_url": "<string>",
"details": [
{}
],
"retry_after": 2,
"reset_at": "2026-10-14T17:01:00.000Z"
},
"trace_id": "trc_8f2a1c0e4b7d",
"timestamp": "2026-10-14T17:01:00.000Z",
"client_request_id": "<string>"
}{
"status": "error",
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "<string>",
"type": "validation",
"http_status": 400,
"retryable": true,
"doc_url": "<string>",
"details": [
{}
],
"retry_after": 2,
"reset_at": "2026-10-14T17:01:00.000Z"
},
"trace_id": "trc_8f2a1c0e4b7d",
"timestamp": "2026-10-14T17:01:00.000Z",
"client_request_id": "<string>"
}Edit a message
Changes the text of an iMessage you sent from this workspace. The edit also shows on the recipient’s device.
Apple’s limits apply: you can edit a message within 15 minutes of sending it, up to 5 times. Only text-only messages can be edited, so part_index is always 0.
If the message already has the text you send, Blujay returns the message without editing it again. Send an Idempotency-Key so a retry after a lost response is safe.
To hear about every edit, including edits your contacts make, subscribe to the message.edited webhook.
If editing is switched off for your workspace, the request returns 400 FEATURE_NOT_ENABLED.
curl --request PATCH \
--url https://api.blujay.io/v1/messages/{message_id} \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"text": "See you at 4 instead"
}
'const options = {
method: 'PATCH',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({text: 'See you at 4 instead'})
};
fetch('https://api.blujay.io/v1/messages/{message_id}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.blujay.io/v1/messages/{message_id}"
payload = { "text": "See you at 4 instead" }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.patch(url, json=payload, headers=headers)
print(response.text)package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.blujay.io/v1/messages/{message_id}"
payload := strings.NewReader("{\n \"text\": \"See you at 4 instead\"\n}")
req, _ := http.NewRequest("PATCH", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}require 'uri'
require 'net/http'
url = URI("https://api.blujay.io/v1/messages/{message_id}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Patch.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"text\": \"See you at 4 instead\"\n}"
response = http.request(request)
puts response.read_body<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.blujay.io/v1/messages/{message_id}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PATCH",
CURLOPT_POSTFIELDS => json_encode([
'text' => 'See you at 4 instead'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}{
"status": "success",
"trace_id": "trc_8f2a1c0e4b7d",
"timestamp": "2026-10-14T17:01:00.000Z",
"data": {
"message_id": "msg_abc123",
"chat_id": "chat_abc123",
"phone_number_id": "phone_abc123",
"direction": "inbound",
"preferred_service": "iMessage",
"service": "iMessage",
"status": "received",
"parts": [
{
"type": "text",
"text": "Hi Ava, this is Sam from the Northwind support team.",
"text_decorations": [
{
"range": [
1,
1
],
"style": "bold"
}
]
}
],
"degradations": [
{
"code": "effect_not_delivered",
"reason": "The iMessage effect was not delivered because the message was sent as SMS or RCS."
}
],
"reply_to": {
"message_id": "msg_abc123",
"part_index": 0,
"thread_root_message_id": "msg_abc123"
},
"sent_at": "2026-10-14T17:01:02.000Z",
"delivered_at": "2026-10-14T17:01:03.000Z",
"read_at": "2023-11-07T05:31:56Z",
"failed_at": "2023-11-07T05:31:56Z",
"failure": {
"code": "recipient_unavailable",
"reason": "The messaging network rejected the message."
},
"edited_at": "2026-10-14T17:01:00.000Z",
"edit_count": 0,
"unsent_at": "2026-10-14T17:01:00.000Z",
"unsent_scope": "all",
"created_at": "2026-10-14T17:01:00.000Z",
"updated_at": "2026-10-14T17:01:00.000Z",
"contact_id": "contact_abc123",
"sender": {
"type": "phone",
"address": "<string>",
"contact_id": "contact_abc123"
},
"failure_code": "ATTACHMENT_FETCH_FAILED",
"effect": {
"type": "screen",
"name": "confetti"
},
"retryable": true,
"client_reference": "<string>",
"edit_history": [
{
"text": "See you at 3",
"replaced_at": "2026-10-14T17:01:00.000Z"
}
],
"trace_id": "trc_8f2a1c0e4b7d"
},
"client_request_id": "<string>"
}{
"status": "error",
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "<string>",
"type": "validation",
"http_status": 400,
"retryable": true,
"doc_url": "<string>",
"details": [
{}
],
"retry_after": 2,
"reset_at": "2026-10-14T17:01:00.000Z"
},
"trace_id": "trc_8f2a1c0e4b7d",
"timestamp": "2026-10-14T17:01:00.000Z",
"client_request_id": "<string>"
}{
"status": "error",
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "<string>",
"type": "validation",
"http_status": 400,
"retryable": true,
"doc_url": "<string>",
"details": [
{}
],
"retry_after": 2,
"reset_at": "2026-10-14T17:01:00.000Z"
},
"trace_id": "trc_8f2a1c0e4b7d",
"timestamp": "2026-10-14T17:01:00.000Z",
"client_request_id": "<string>"
}{
"status": "error",
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "<string>",
"type": "validation",
"http_status": 400,
"retryable": true,
"doc_url": "<string>",
"details": [
{}
],
"retry_after": 2,
"reset_at": "2026-10-14T17:01:00.000Z"
},
"trace_id": "trc_8f2a1c0e4b7d",
"timestamp": "2026-10-14T17:01:00.000Z",
"client_request_id": "<string>"
}{
"status": "error",
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "<string>",
"type": "validation",
"http_status": 400,
"retryable": true,
"doc_url": "<string>",
"details": [
{}
],
"retry_after": 2,
"reset_at": "2026-10-14T17:01:00.000Z"
},
"trace_id": "trc_8f2a1c0e4b7d",
"timestamp": "2026-10-14T17:01:00.000Z",
"client_request_id": "<string>"
}{
"status": "error",
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "<string>",
"type": "validation",
"http_status": 400,
"retryable": true,
"doc_url": "<string>",
"details": [
{}
],
"retry_after": 2,
"reset_at": "2026-10-14T17:01:00.000Z"
},
"trace_id": "trc_8f2a1c0e4b7d",
"timestamp": "2026-10-14T17:01:00.000Z",
"client_request_id": "<string>"
}Authorizations
Your workspace API key, sent as Authorization: Bearer blu_.... Workspace admins can find it in the Blujay app under API.
An API key has full access to your workspace and works on every endpoint. There is no test mode: every key sends real messages and changes real data, so keep it secret and use it only from your server.
Each API key can make 600 read requests and 120 write requests per minute, plus 60 attachment requests per minute. Over a limit you get 429 RATE_LIMIT_EXCEEDED with Retry-After.
Headers
A unique value you choose (1–255 characters) that makes this request safe to retry. Send the same key with the same request and Blujay returns the original response instead of doing the work again. Use a new key for each new action.
Blujay keeps keys for at least 24 hours. Keys are separate for each API key and each route family (for example every POST /v1/chats/{chat_id}/messages shares one). Reusing a key with a different request returns 409 IDEMPOTENCY_REQUEST_MISMATCH. Sending it while the first request is still running returns 409 IDEMPOTENCY_IN_PROGRESS; retry shortly.
1 - 255Path Parameters
^msg_[A-Za-z0-9_-]+$"msg_abc123"
Body
The new text. Must contain a non-whitespace character and differ from the current text.
1 - 10000"See you at 4 instead"
Only text-only messages can be edited, so the only editable part is 0.
0 Response
The message after the edit or unsend. If the same change was already in place, the message is returned unchanged.
Wraps every successful response. The result is in data.
success ^trc_[A-Za-z0-9_-]+$"trc_8f2a1c0e4b7d"
"2026-10-14T17:01:00.000Z"
Show child attributes
Show child attributes
Your X-Request-ID header value, echoed back when you send one. Not included on the send responses (POST /v1/chats, POST /v1/chats/{chat_id}/messages, POST /v1/messages/{message_id}/retry); keep your own mapping from your request ID to the returned message ID there.