curl --request POST \
--url https://api.blujay.io/v1/workflows/{workflow_id}/enrollments \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"contact_id": "contact_abc123",
"request_id": "signup-7f1d9d0a8a3d4b4d",
"metadata": {
"source": "signup_form",
"external_id": "A-1042"
}
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
contact_id: 'contact_abc123',
request_id: 'signup-7f1d9d0a8a3d4b4d',
metadata: {source: 'signup_form', external_id: 'A-1042'}
})
};
fetch('https://api.blujay.io/v1/workflows/{workflow_id}/enrollments', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.blujay.io/v1/workflows/{workflow_id}/enrollments"
payload = {
"contact_id": "contact_abc123",
"request_id": "signup-7f1d9d0a8a3d4b4d",
"metadata": {
"source": "signup_form",
"external_id": "A-1042"
}
}
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/workflows/{workflow_id}/enrollments"
payload := strings.NewReader("{\n \"contact_id\": \"contact_abc123\",\n \"request_id\": \"signup-7f1d9d0a8a3d4b4d\",\n \"metadata\": {\n \"source\": \"signup_form\",\n \"external_id\": \"A-1042\"\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/workflows/{workflow_id}/enrollments")
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 \"contact_id\": \"contact_abc123\",\n \"request_id\": \"signup-7f1d9d0a8a3d4b4d\",\n \"metadata\": {\n \"source\": \"signup_form\",\n \"external_id\": \"A-1042\"\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/workflows/{workflow_id}/enrollments",
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([
'contact_id' => 'contact_abc123',
'request_id' => 'signup-7f1d9d0a8a3d4b4d',
'metadata' => [
'source' => 'signup_form',
'external_id' => 'A-1042'
]
]),
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": {
"workflow_id": "workflow_abc123",
"contact_id": "contact_abc123",
"request_id": "enroll-order-4821",
"enrollment_status": "<string>",
"workflow_run": {
"status": "<string>",
"status_reason_code": "<string>"
},
"no_op_reason": "<string>"
},
"client_request_id": "<string>"
}{
"status": "success",
"trace_id": "trc_8f2a1c0e4b7d",
"timestamp": "2026-10-14T17:01:00.000Z",
"data": {
"workflow_id": "workflow_abc123",
"contact_id": "contact_abc123",
"request_id": "enroll-order-4821",
"enrollment_status": "<string>",
"workflow_run": {
"status": "<string>",
"status_reason_code": "<string>"
},
"no_op_reason": "<string>"
},
"client_request_id": "<string>"
}{
"status": "error",
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"type": "validation",
"http_status": 400,
"retryable": false,
"doc_url": "https://docs.blujay.io/errors/validation-error",
"details": []
},
"trace_id": "trc_8f2a1c0e4b7d",
"timestamp": "2026-10-14T00: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": "WORKFLOW_LIFECYCLE_ERROR",
"message": "Workflow must be active before it can accept manual enrollment",
"type": "validation",
"http_status": 409,
"retryable": false,
"doc_url": "https://docs.blujay.io/errors/workflow-lifecycle-error",
"details": []
},
"trace_id": "trc_8f2a1c0e4b7d",
"timestamp": "2026-10-14T00:00:00.000Z"
}{
"status": "error",
"error": {
"code": "WORKFLOW_VALIDATION_ERROR",
"message": "Workflow requires a published version before it can accept manual enrollment",
"type": "validation",
"http_status": 422,
"retryable": false,
"doc_url": "https://docs.blujay.io/errors/workflow-validation-error",
"details": []
},
"trace_id": "trc_8f2a1c0e4b7d",
"timestamp": "2026-10-14T00:00:00.000Z"
}Enroll a contact in a workflow
Starts a manual workflow for one contact.
request_id is required and makes retries safe: sending the same request_id again returns 200 with the original result instead of enrolling the contact twice. A 200 can also mean no new run started; enrollment_status and no_op_reason say what happened.
metadata is optional context about the enrollment. It isn’t available as workflow variables.
Returns 409 WORKFLOW_LIFECYCLE_ERROR when the workflow isn’t active or doesn’t start manually, and 422 WORKFLOW_VALIDATION_ERROR when the workflow can’t run yet, for example because it has no published version.
curl --request POST \
--url https://api.blujay.io/v1/workflows/{workflow_id}/enrollments \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"contact_id": "contact_abc123",
"request_id": "signup-7f1d9d0a8a3d4b4d",
"metadata": {
"source": "signup_form",
"external_id": "A-1042"
}
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
contact_id: 'contact_abc123',
request_id: 'signup-7f1d9d0a8a3d4b4d',
metadata: {source: 'signup_form', external_id: 'A-1042'}
})
};
fetch('https://api.blujay.io/v1/workflows/{workflow_id}/enrollments', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.blujay.io/v1/workflows/{workflow_id}/enrollments"
payload = {
"contact_id": "contact_abc123",
"request_id": "signup-7f1d9d0a8a3d4b4d",
"metadata": {
"source": "signup_form",
"external_id": "A-1042"
}
}
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/workflows/{workflow_id}/enrollments"
payload := strings.NewReader("{\n \"contact_id\": \"contact_abc123\",\n \"request_id\": \"signup-7f1d9d0a8a3d4b4d\",\n \"metadata\": {\n \"source\": \"signup_form\",\n \"external_id\": \"A-1042\"\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/workflows/{workflow_id}/enrollments")
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 \"contact_id\": \"contact_abc123\",\n \"request_id\": \"signup-7f1d9d0a8a3d4b4d\",\n \"metadata\": {\n \"source\": \"signup_form\",\n \"external_id\": \"A-1042\"\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/workflows/{workflow_id}/enrollments",
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([
'contact_id' => 'contact_abc123',
'request_id' => 'signup-7f1d9d0a8a3d4b4d',
'metadata' => [
'source' => 'signup_form',
'external_id' => 'A-1042'
]
]),
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": {
"workflow_id": "workflow_abc123",
"contact_id": "contact_abc123",
"request_id": "enroll-order-4821",
"enrollment_status": "<string>",
"workflow_run": {
"status": "<string>",
"status_reason_code": "<string>"
},
"no_op_reason": "<string>"
},
"client_request_id": "<string>"
}{
"status": "success",
"trace_id": "trc_8f2a1c0e4b7d",
"timestamp": "2026-10-14T17:01:00.000Z",
"data": {
"workflow_id": "workflow_abc123",
"contact_id": "contact_abc123",
"request_id": "enroll-order-4821",
"enrollment_status": "<string>",
"workflow_run": {
"status": "<string>",
"status_reason_code": "<string>"
},
"no_op_reason": "<string>"
},
"client_request_id": "<string>"
}{
"status": "error",
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"type": "validation",
"http_status": 400,
"retryable": false,
"doc_url": "https://docs.blujay.io/errors/validation-error",
"details": []
},
"trace_id": "trc_8f2a1c0e4b7d",
"timestamp": "2026-10-14T00: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": "WORKFLOW_LIFECYCLE_ERROR",
"message": "Workflow must be active before it can accept manual enrollment",
"type": "validation",
"http_status": 409,
"retryable": false,
"doc_url": "https://docs.blujay.io/errors/workflow-lifecycle-error",
"details": []
},
"trace_id": "trc_8f2a1c0e4b7d",
"timestamp": "2026-10-14T00:00:00.000Z"
}{
"status": "error",
"error": {
"code": "WORKFLOW_VALIDATION_ERROR",
"message": "Workflow requires a published version before it can accept manual enrollment",
"type": "validation",
"http_status": 422,
"retryable": false,
"doc_url": "https://docs.blujay.io/errors/workflow-validation-error",
"details": []
},
"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.
Path Parameters
^workflow_[A-Za-z0-9_-]+$"workflow_abc123"
Body
^contact_[A-Za-z0-9_-]+$"contact_abc123"
Your unique ID for this enrollment. Sending the same request_id again returns the original result instead of enrolling the contact twice.
1 - 200"enroll-order-4821"
Optional context about the enrollment. Up to 20 keys, each a string (up to 500 characters), number, boolean or null, and 4,096 bytes in total.
Show child attributes
Show child attributes
Response
This request_id was already used, or no new run started. Check enrollment_status and no_op_reason.
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.