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:

FieldRequiredTypeConstraint
appIdyesstringNon-empty. Must be in your key’s appIds[].
contactIdyesstringNon-empty. Format depends on messenger (see below).
messengeryesenumsignal | simplex | session.
messageyesstring1–10,000 characters after trim.
groupIdnostringTrimmed. Include only for group replies — see Concepts.

Batch constraints

  • The messages array 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 failedMessages with an error string; valid ones enqueue normally. The HTTP status reflects the batch-level outcome, not per-message.

Validation errors per message

Triggererror
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.

MessengerFormatExample
signalE.164 phone number or registered username+12025550104
simplexNumeric SimpleX user id3
session66-character hex Session ID starting 05054c46f5e7b1a3cf9b2d8e6f1c4a5b7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4

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.

MessengerDisplay limit (chars)
signal2,000
simplex8,000
session2,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

FieldTypeNotes
successfulMessagesSuccessfulMessage[]Each carries the server-assigned messageId (32-char hex).
failedMessagesFailedMessage[]Each carries the offending input and an error string.
totalAcceptedintCount of successfully enqueued messages.
totalFailedintCount of messages rejected.

Status codes

CodeWhen
202At least one message was accepted. (Failed items appear in failedMessages.)
400Invalid JSON, no messages array, empty array, batch over 50, or any per-message schema error that fails before any enqueue is attempted.
401Missing header, wrong-header-for-credential-type.
403Key recognized but invalid.
429Rate-limited — counted per-message in the batch.
503BullMQ 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.

Last updated: