Send messages
POST /api/v1/messages/send — enqueue encrypted messages for delivery, with per-message validation and a success/failure split response.
POST /api/v1/messages/send queues encrypted messages for delivery. Send up to 50 messages per call; the API validates each one independently and returns a partial-success envelope. A 202 Accepted means “accepted into the queue” — delivery to the recipient is asynchronous and not confirmed by this endpoint.
Request
POST /api/v1/messages/send HTTP/1.1
Host: privacyflow.app
Authorization: Bearer pf_live_your_key_here
Content-Type: application/json
{
"messages": [
{
"appId": "app_123",
"contactId": "+12025550104",
"messenger": "signal",
"message": "Your order #4421 has shipped."
}
]
}
Auth required. The body must be a JSON object with a messages array — top-level arrays and bare objects are rejected with 400.
curl
curl -s -X POST https://privacyflow.app/api/v1/messages/send \
-H "Authorization: Bearer $PRIVACYFLOW_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
"appId": "'"$PRIVACYFLOW_APP_ID"'",
"contactId": "+12025550104",
"messenger": "signal",
"message": "Your order #4421 has shipped."
}
]
}'
Request body: SendMessageInput
Each item in messages:
| Field | Required | Type | Constraint |
|---|---|---|---|
appId | yes | string | Non-empty. Must be in your key’s appIds[]. |
contactId | yes | string | Non-empty. Format depends on messenger (see below). |
messenger | yes | enum | signal | simplex | session. |
message | yes | string | 1–10,000 characters after trim. |
groupId | no | string | Trimmed. Include only for group replies — see Concepts. |
Batch constraints
- The
messagesarray must contain at least one item (400 "At least one message is required"). - The array must contain at most 50 items (
400 "Batch size exceeds maximum of 50 messages"). - Each item is validated independently. Invalid items are returned in
failedMessageswith anerrorstring; valid ones enqueue normally. The HTTP status reflects the batch-level outcome, not per-message.
Validation errors per message
| Trigger | error |
|---|---|
appId empty | "Missing required field: appId" |
contactId empty | "Missing required field: contactId" |
message empty or whitespace-only | "Missing required field: message" |
message > 10000 chars | "Message exceeds 10000 character limit" |
messenger not in enum | "Invalid messenger. Must be one of: signal, simplex, session" |
appId not in key’s appIds[] | "AppId not associated with this API key" |
Contact ID formats per messenger
The API validates only that contactId is non-empty. The actual format is enforced downstream by each messenger. Mismatched formats produce per-message failures in the response rather than a 400 at the API layer.
| Messenger | Format | Example |
|---|---|---|
signal | E.164 phone number or registered username | +12025550104 |
simplex | Numeric SimpleX user id | 3 |
session | 66-character hex Session ID starting 05 | 054c46f5e7b1a3cf9b2d8e6f1c4a5b7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4 |
Character limits per messenger
The API caps message at 10,000 characters. Each messenger has a smaller display limit and will reject (or truncate) over-length payloads downstream. The Agent Zero channel splits long replies before sending — your client should do the same when you expect long output.
| Messenger | Display limit (chars) |
|---|---|
signal | 2,000 |
simplex | 8,000 |
session | 2,000 |
Group replies
For a reply to land in the same thread as a group inbound, pass the groupId from the polled message:
{
"messages": [
{
"appId": "app_123",
"contactId": "05abcd…",
"messenger": "session",
"message": "Got it — assigning an agent.",
"groupId": "group_abc123"
}
]
}
For DMs, omit groupId entirely. Sending a groupId against a direct message is undefined.
Response
202 Accepted:
{
"successfulMessages": [
{
"messageId": "8a5b3c0e1d2f4a6b8c0d1e2f3a4b5c6d",
"appId": "app_123",
"contactId": "+12025550104",
"messenger": "signal"
}
],
"failedMessages": [
{
"appId": "app_123",
"contactId": "invalid-format",
"messenger": "signal",
"error": "Missing required field: contactId"
}
],
"totalAccepted": 1,
"totalFailed": 1
}
Response schema
| Field | Type | Notes |
|---|---|---|
successfulMessages | SuccessfulMessage[] | Each carries the server-assigned messageId (32-char hex). |
failedMessages | FailedMessage[] | Each carries the offending input and an error string. |
totalAccepted | int | Count of successfully enqueued messages. |
totalFailed | int | Count of messages rejected. |
Status codes
| Code | When |
|---|---|
202 | At least one message was accepted. (Failed items appear in failedMessages.) |
400 | Invalid JSON, no messages array, empty array, batch over 50, or any per-message schema error that fails before any enqueue is attempted. |
401 | Missing header, wrong-header-for-credential-type. |
403 | Key recognized but invalid. |
429 | Rate-limited — counted per-message in the batch. |
503 | BullMQ enqueue failure on the backend. |
Rate-limit accounting
The send limit counts messages in the batch, not requests. A 50-message POST consumes 50 units. If the batch alone would exceed the remaining budget the entire request is rejected before any enqueue — see rate limiting.
What 202 doesn’t tell you
A 202 response means the API accepted the batch and handed it to BullMQ. It does not mean the message was delivered to the recipient. Downstream delivery happens asynchronously with up to 3 attempts and exponential backoff. The public API surfaces no delivery-status endpoint — if you need confirmation of delivery, build it on your side (e.g., an inbound /delivered command from the recipient).
Warning
Replying to a polled message requires the exact messenger, contactId, and (if applicable) groupId from the inbound payload. The n8n trigger node warns about this in its UI — see the n8n client guide.