curl --request DELETE \
--url https://api.blujay.io/v1/chats/{chat_id}/participants \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"handle": "+15555550123",
"from_phone_number_id": "phone_abc123"
}
'const options = {
method: 'DELETE',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({handle: '+15555550123', from_phone_number_id: 'phone_abc123'})
};
fetch('https://api.blujay.io/v1/chats/{chat_id}/participants', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.blujay.io/v1/chats/{chat_id}/participants"
payload = {
"handle": "+15555550123",
"from_phone_number_id": "phone_abc123"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.delete(url, json=payload, headers=headers)
print(response.text)package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.blujay.io/v1/chats/{chat_id}/participants"
payload := strings.NewReader("{\n \"handle\": \"+15555550123\",\n \"from_phone_number_id\": \"phone_abc123\"\n}")
req, _ := http.NewRequest("DELETE", 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/chats/{chat_id}/participants")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Delete.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"handle\": \"+15555550123\",\n \"from_phone_number_id\": \"phone_abc123\"\n}"
response = http.request(request)
puts response.read_body<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.blujay.io/v1/chats/{chat_id}/participants",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "DELETE",
CURLOPT_POSTFIELDS => json_encode([
'handle' => '+15555550123',
'from_phone_number_id' => 'phone_abc123'
]),
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": {
"chat_id": "chat_abc123",
"operation": "participant_add",
"status": "accepted",
"participant": {
"handle": "+15555550124",
"type": "phone",
"status": "active"
},
"from_phone_number_id": "phone_abc123"
},
"client_request_id": "<string>"
}{
"status": "success",
"data": {
"chat_id": "chat_abc123",
"operation": "participant_remove",
"status": "accepted",
"participant": {
"handle": "+15555550123",
"type": "phone",
"status": "left"
},
"from_phone_number_id": "phone_abc123"
},
"trace_id": "trc_8f2a1c0e4b7d",
"timestamp": "2026-10-14T22:00:00.000Z"
}{
"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": "PHONE_NUMBER_UNAVAILABLE",
"message": "This service is temporarily unavailable",
"type": "phone_number",
"http_status": 503,
"retryable": true,
"doc_url": "https://docs.blujay.io/errors/phone-number-unavailable",
"details": [],
"retry_after": 30
},
"trace_id": "trc_8f2a1c0e4b7d",
"timestamp": "2026-10-14T00:00:00.000Z"
}Remove a group participant
Removes a phone number or email address from an active iMessage group chat. If more than one of your Blujay numbers is in the group, set from_phone_number_id to choose which one makes the change.
Removing someone who already left returns 200 with status: already_removed; removing someone who was never in the group returns 404 NOT_FOUND. Neither contacts the phone. To take one of your own numbers out of a group, use POST /v1/chats/{chat_id}/leave instead. Send an Idempotency-Key so you can safely retry.
curl --request DELETE \
--url https://api.blujay.io/v1/chats/{chat_id}/participants \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"handle": "+15555550123",
"from_phone_number_id": "phone_abc123"
}
'const options = {
method: 'DELETE',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({handle: '+15555550123', from_phone_number_id: 'phone_abc123'})
};
fetch('https://api.blujay.io/v1/chats/{chat_id}/participants', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.blujay.io/v1/chats/{chat_id}/participants"
payload = {
"handle": "+15555550123",
"from_phone_number_id": "phone_abc123"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.delete(url, json=payload, headers=headers)
print(response.text)package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.blujay.io/v1/chats/{chat_id}/participants"
payload := strings.NewReader("{\n \"handle\": \"+15555550123\",\n \"from_phone_number_id\": \"phone_abc123\"\n}")
req, _ := http.NewRequest("DELETE", 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/chats/{chat_id}/participants")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Delete.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"handle\": \"+15555550123\",\n \"from_phone_number_id\": \"phone_abc123\"\n}"
response = http.request(request)
puts response.read_body<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.blujay.io/v1/chats/{chat_id}/participants",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "DELETE",
CURLOPT_POSTFIELDS => json_encode([
'handle' => '+15555550123',
'from_phone_number_id' => 'phone_abc123'
]),
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": {
"chat_id": "chat_abc123",
"operation": "participant_add",
"status": "accepted",
"participant": {
"handle": "+15555550124",
"type": "phone",
"status": "active"
},
"from_phone_number_id": "phone_abc123"
},
"client_request_id": "<string>"
}{
"status": "success",
"data": {
"chat_id": "chat_abc123",
"operation": "participant_remove",
"status": "accepted",
"participant": {
"handle": "+15555550123",
"type": "phone",
"status": "left"
},
"from_phone_number_id": "phone_abc123"
},
"trace_id": "trc_8f2a1c0e4b7d",
"timestamp": "2026-10-14T22:00:00.000Z"
}{
"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": "PHONE_NUMBER_UNAVAILABLE",
"message": "This service is temporarily unavailable",
"type": "phone_number",
"http_status": 503,
"retryable": true,
"doc_url": "https://docs.blujay.io/errors/phone-number-unavailable",
"details": [],
"retry_after": 30
},
"trace_id": "trc_8f2a1c0e4b7d",
"timestamp": "2026-10-14T00:00:00.000Z"
}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
^chat_[A-Za-z0-9_-]+$"chat_abc123"
Body
from_phone_number_id picks which of your numbers makes the change. It is optional only when one of your numbers is in the group.
Response
This Idempotency-Key was already used for the same request. The response is the original one.
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.