curl --request POST \
--url https://api.blujay.io/v1/webhook-subscriptions \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"url": "https://example.com/webhooks/blujay",
"event_types": [
"message.accepted",
"message.sent",
"message.delivered",
"message.read",
"message.failed"
]
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
url: 'https://example.com/webhooks/blujay',
event_types: [
'message.accepted',
'message.sent',
'message.delivered',
'message.read',
'message.failed'
]
})
};
fetch('https://api.blujay.io/v1/webhook-subscriptions', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.blujay.io/v1/webhook-subscriptions"
payload = {
"url": "https://example.com/webhooks/blujay",
"event_types": ["message.accepted", "message.sent", "message.delivered", "message.read", "message.failed"]
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.blujay.io/v1/webhook-subscriptions"
payload := strings.NewReader("{\n \"url\": \"https://example.com/webhooks/blujay\",\n \"event_types\": [\n \"message.accepted\",\n \"message.sent\",\n \"message.delivered\",\n \"message.read\",\n \"message.failed\"\n ]\n}")
req, _ := http.NewRequest("POST", 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/webhook-subscriptions")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"url\": \"https://example.com/webhooks/blujay\",\n \"event_types\": [\n \"message.accepted\",\n \"message.sent\",\n \"message.delivered\",\n \"message.read\",\n \"message.failed\"\n ]\n}"
response = http.request(request)
puts response.read_body<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.blujay.io/v1/webhook-subscriptions",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'url' => 'https://example.com/webhooks/blujay',
'event_types' => [
'message.accepted',
'message.sent',
'message.delivered',
'message.read',
'message.failed'
]
]),
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",
"data": {
"subscription_id": "whsub_abc123",
"url": "https://example.com/webhooks/blujay",
"event_types": [
"message.accepted",
"message.sent",
"message.delivered",
"message.read",
"message.failed"
],
"status": "active",
"phone_number_filter": {
"mode": "all"
},
"webhook_version": "2026-10-03",
"signing_secret_hint": "Y3OA==",
"created_at": "2026-10-14T17:01:00.000Z",
"updated_at": "2026-10-14T17:01:00.000Z",
"signing_secret": "whsec_ZXhhbXBsZS1zZWNyZXQtZG8tbm90LXVzZS0xMjM0NTY3OA=="
},
"trace_id": "trc_8f2a1c0e4b7d",
"timestamp": "2026-10-14T17:01:00.000Z"
}Create a webhook subscription
Subscribes an HTTPS endpoint to the events you choose. The response includes signing_secret, which is shown only this once. Store it securely; you need it to verify webhook signatures. To change the secret, create a new subscription and delete the old one.
New subscriptions start active unless you set status: disabled. Leave out phone_number_filter to get events for all of your numbers, including numbers you add later.
The URL must use HTTPS and resolve to a public address.
Errors worth handling (all 400):
UNSUPPORTED_WEBHOOK_EVENT: an event type isn’t recognized.FEATURE_NOT_ENABLED: an event type is recognized but can’t be subscribed to.WEBHOOK_URL_INVALID,WEBHOOK_URL_UNSAFEorWEBHOOK_URL_UNRESOLVABLE: the URL isn’t HTTPS, points to a private address, or doesn’t resolve.
curl --request POST \
--url https://api.blujay.io/v1/webhook-subscriptions \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"url": "https://example.com/webhooks/blujay",
"event_types": [
"message.accepted",
"message.sent",
"message.delivered",
"message.read",
"message.failed"
]
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
url: 'https://example.com/webhooks/blujay',
event_types: [
'message.accepted',
'message.sent',
'message.delivered',
'message.read',
'message.failed'
]
})
};
fetch('https://api.blujay.io/v1/webhook-subscriptions', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.blujay.io/v1/webhook-subscriptions"
payload = {
"url": "https://example.com/webhooks/blujay",
"event_types": ["message.accepted", "message.sent", "message.delivered", "message.read", "message.failed"]
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.blujay.io/v1/webhook-subscriptions"
payload := strings.NewReader("{\n \"url\": \"https://example.com/webhooks/blujay\",\n \"event_types\": [\n \"message.accepted\",\n \"message.sent\",\n \"message.delivered\",\n \"message.read\",\n \"message.failed\"\n ]\n}")
req, _ := http.NewRequest("POST", 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/webhook-subscriptions")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"url\": \"https://example.com/webhooks/blujay\",\n \"event_types\": [\n \"message.accepted\",\n \"message.sent\",\n \"message.delivered\",\n \"message.read\",\n \"message.failed\"\n ]\n}"
response = http.request(request)
puts response.read_body<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.blujay.io/v1/webhook-subscriptions",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'url' => 'https://example.com/webhooks/blujay',
'event_types' => [
'message.accepted',
'message.sent',
'message.delivered',
'message.read',
'message.failed'
]
]),
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",
"data": {
"subscription_id": "whsub_abc123",
"url": "https://example.com/webhooks/blujay",
"event_types": [
"message.accepted",
"message.sent",
"message.delivered",
"message.read",
"message.failed"
],
"status": "active",
"phone_number_filter": {
"mode": "all"
},
"webhook_version": "2026-10-03",
"signing_secret_hint": "Y3OA==",
"created_at": "2026-10-14T17:01:00.000Z",
"updated_at": "2026-10-14T17:01:00.000Z",
"signing_secret": "whsec_ZXhhbXBsZS1zZWNyZXQtZG8tbm90LXVzZS0xMjM0NTY3OA=="
},
"trace_id": "trc_8f2a1c0e4b7d",
"timestamp": "2026-10-14T17:01: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.
Body
Your HTTPS endpoint, up to 2,048 characters. It must resolve to a public address; localhost and private, link-local and cloud metadata addresses are rejected.
2048"https://example.com/webhooks/blujay"
The events to send to this endpoint (1–100).
1 - 100 elementsEvents you can subscribe to.
- Outbound message events (
message.sent,message.delivered,message.read,message.failed) cover messages sent through Blujay: the app, workflows, scheduled sends, integrations and the API.message.acceptedis sent only for API sends. Messages sent outside Blujay aren't included. message.receivedcovers new incoming direct and group messages, not older messages Blujay imports later.phone_number.status_updatedis currently sent only when one of your numbers is replaced by a new dedicated number: the old number becomesunavailableand the new oneactive.
message.accepted, message.sent, message.delivered, message.read, message.failed, message.received, reaction.added, reaction.removed, phone_number.status_updated, contact.tag_added, contact.assigned, chat.typing_indicator.started, chat.typing_indicator.stopped, participant.added, participant.removed, chat.group_name_updated, message.edited, message.unsent Which of your numbers this subscription gets events for. all covers every number, including numbers you add later; selected covers only the phone_number_ids you list. Contact events are always sent for the whole workspace. Defaults to all on create; left unchanged on update if you omit it.
- Option 1
- Option 2
Show child attributes
Show child attributes
The payload version for this subscription's events. When omitted, new subscriptions use 2026-10-03, which adds iMessage group typing with participants and otherwise matches 2026-09-02. Existing subscriptions keep their version until you change it.
2026-06-14, 2026-08-01, 2026-08-20, 2026-09-02, 2026-10-03 Defaults to active. A disabled subscription doesn't receive new events.
active, disabled Response
The new subscription, including its signing_secret.
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.