---
title: "Create conversation"
method: POST
path: "/v1/inbox/conversations"
tags: ["Messages"]
---

# Create conversation

`POST /v1/inbox/conversations`

Initiate a new direct message conversation with a specified user. If a conversation already exists with the recipient, the message is added to the existing thread.

Supported platforms: X/Twitter, Bluesky, Reddit, WhatsApp, SMS, and Slack. Other platforms return PLATFORM_NOT_SUPPORTED.

Slack: pass a workspace member id as participantId (list them with GET /v1/accounts/{accountId}/slack-members). Zernio opens the DM channel with that member and sends the message; the thread then behaves like any other Slack conversation in the inbox. The member must belong to the connected workspace.

WhatsApp: this is the endpoint for sending an approved template message to a phone number. Provide templateName, templateLanguage, and templateParams (variable values for the text header, body and dynamic URL buttons, in that order), with the recipient phone in participantId. A template is required because WhatsApp does not permit freeform messages to open a conversation; a missing template returns TEMPLATE_REQUIRED. Templates with media headers (image, video, document) are handled automatically: Zernio reads the approved template definition and fills the header at send time with the template's approved sample asset. To send a DIFFERENT asset per message (e.g. a distinct invoice PDF for each recipient), pass the headerMedia field with a public link (or a Meta media id); it overrides the sample for that send. A template whose approved header format is LOCATION has no header asset to reconstruct at all: Meta only accepts the location at send time, so pass headerLocation (latitude and longitude required) whenever such a template is sent; headerMedia and headerLocation cannot both be supplied. A button that carries its own value at send time (a copy-code button holding a Pix payment code or a coupon, a flow token) is sent with templateButtonParams, addressed by the button's index; templateParams covers text variables and dynamic URL buttons only. Calling this for a number you already have a thread with simply sends the template into that thread, which also makes it the way to re-engage a contact after the 24-hour customer-service window has closed. Once the recipient replies (opening the 24h window), send freeform messages with the send-message endpoint (POST /v1/inbox/conversations/{conversationId}/messages). Template fields are accepted on the JSON body only, not on multipart requests. Alternatively, WhatsApp Business Accounts eligible for Meta Direct Send can open a conversation with a business-initiated utility text message and no template: pass category: 'utility' together with message (and no templateName). See the category field below.

DM eligibility (X/Twitter): Before sending, the endpoint checks if the recipient accepts DMs from your account (via the receives_your_dm field). If not, a 422 error with code DM_NOT_ALLOWED is returned. You can skip this check with skipDmCheck: true if you have already verified eligibility.

X API tier requirement: DM write endpoints require X API Pro tier ($5,000/month) or Enterprise access. This applies to BYOK (Bring Your Own Key) users who provide their own X API credentials.

Rate limits (X/Twitter only): X's DM API enforces 200 requests per 15 minutes, 1,000 per 24 hours per connected X account, and 15,000 per 24 hours per X developer app (shared across all DM endpoints). These limits do NOT apply to other platforms. WhatsApp sends are governed by Meta's per-number messaging tiers (unique business-initiated conversations per 24 hours) and per-number throughput instead.

## Request body

- object
  - `accountId` string, required — The social account ID to send from
  - `participantId` string — Recipient identifier. For X this is the numeric user ID; for WhatsApp and SMS, the recipient phone number in international format (digits, country code included); for Slack, the workspace member id (e.g. U01ABCDEF). Provide either this or participantUsername.
  - `participantUsername` string — Recipient handle/username — an X or Bluesky handle (with or without @) or a Reddit username (with or without u/). Resolved via lookup. Provide either this or participantId.
  - `message` string — Text content of the message. At least one of message, attachment, or (for WhatsApp) templateName is required. Required when category is set (a Direct Send utility message is a text message).
  - `skipDmCheck` boolean — X/Twitter only. Skip the receives_your_dm eligibility check before sending. Use if you have already verified the recipient accepts DMs.
  - `templateName` string — WhatsApp only. Name of the approved template to start the conversation with. Required for WhatsApp unless category is used instead (Direct Send). Cannot be combined with category.
  - `category` 'utility' — WhatsApp only (Meta Direct Send). Combined with message and without templateName, starts the conversation with a business-initiated UTILITY message and no pre-approved template; Meta matches or auto-creates a template asynchronously. The WhatsApp Business Account must be eligible for Direct Send, otherwise the send fails with an error telling you to use an approved message template instead. Cannot be combined with templateName (templates are already categorized at creation). Utility messages only; marketing content is not allowed under this category. Accepted on the JSON body only, not on multipart requests.
  - `linkPreview` boolean — WhatsApp only. Set false to send the Direct Send (category: 'utility') text message without a link-preview thumbnail for the first URL in the text. Defaults to true, which is how every WhatsApp text has been sent to date. Does not apply to template sends. Accepted on the JSON body only, not on multipart requests.
  - `templateLanguage` string — WhatsApp only. Template language code (e.g. en_US).
  - `templateParams` string[] — WhatsApp only. Template variable values as one flat array, in the order the variables appear across the whole template: text-header variables first, then body variables, then one value per dynamic URL button (in button order). Works with positional placeholders ({{1}}, {{2}}, ...) and with named placeholders ({{name}}, {{company}} - how Meta Business Manager creates templates), where values fill the named slots in order of appearance. Example - a body with {{1}}, {{2}} plus a URL button https://example.com/{{1}} takes three values: [body1, body2, buttonSuffix]. For positional templates the list must cover every slot: supplying fewer values than the template's header + body + dynamic URL-button count is rejected with a 400 (code INVALID_TEMPLATE_PARAMS) naming the expected split, rather than delivering a template whose button URL was filled from the wrong value. A dynamic URL button covered by templateButtonParams needs no value here unless another uncovered dynamic URL button follows it, since the override applies after slot numbering. Media headers (image, video, document) are filled automatically from the approved template and take no value here (use headerMedia to override the header asset per send). Buttons that are not dynamic-URL buttons (copy-code, flow) take no value here either; use templateButtonParams.
  - `templateButtonParams` object[] — WhatsApp only. Values for template buttons that carry one at send time, each addressed by the button's position in the approved template. This is the only way to send a copy-code button's payload (a Pix payment code, a coupon) or a flow token, because templateParams is a flat array of text variables and covers dynamic URL buttons only. Supplying a button here overrides whatever templateParams would have derived for that same index, so the send never carries one button twice; repeating an index within this array is rejected with 400. Each index must name a button of the matching kind on the approved template, which is also checked before the send and returns 400 (INVALID_TEMPLATE_BUTTON_PARAM) rather than a Meta rejection.
    - `index` integer, required — Zero-based position of the button in the approved template's buttons.
    - `subType` 'url' | 'copy_code' | 'flow', required — The button kind, which decides how the value is sent: copy_code sends it as the coupon_code payload, flow as the flow token, url as the dynamic suffix appended to the button's base URL.
    - `value` string, required — The value to send (e.g. the Pix copy-and-paste code for a copy_code button).
  - `headerMedia` object — WhatsApp only. Overrides a media-header template's header asset for THIS send, so a template with an image/video/document header can carry a different asset per message (e.g. each recipient their own invoice PDF). Without it, the template's approved sample asset is sent. Provide exactly one of link or id.
    - `type` 'image' | 'video' | 'document', required — Must match the template header's media type.
    - `link` string — Public URL of the asset to send. Must be reachable without auth.
    - `id` string — A Meta media id (from the media upload endpoint), as an alternative to link.
    - `filename` string — Document display name shown to the recipient (e.g. "Factura 0001-123.pdf"). document type only; ignored for image/video.
  - `headerLocation` object — WhatsApp only. Required to send a template whose approved header format is LOCATION: Meta only accepts the location's lat/long at send time, never at template creation, so there is nothing to fill in automatically. Cannot be combined with headerMedia (a template has exactly one header).
    - `latitude` number, required — Latitude in decimal degrees.
    - `longitude` number, required — Longitude in decimal degrees.
    - `name` string — Location name shown to the recipient (e.g. a business name).
    - `address` string — Location address shown to the recipient.

## Response `201`

Conversation created successfully

- object
  - `success` boolean
  - `data` object
    - `messageId` string — Platform message ID (dm_event_id)
    - `conversationId` string — Platform conversation ID (dm_conversation_id). For WhatsApp, this is Zernio's internal conversation id (24-character hex) which matches the id returned by the list-conversations endpoint and the conversationId in the message.received and conversation.started webhooks; use it to correlate the created thread with inbound events.
    - `participantId` string — Twitter numeric user ID of the recipient
    - `participantName` string, nullable — Display name of the recipient
    - `participantUsername` string, nullable — Twitter username of the recipient

## Other responses

- `400` — Validation error, platform not supported, category combined with templateName or used on a non-WhatsApp account, or the WhatsApp Business Account is not eligible for Direct Send
- `401` — Unauthorized
- `403` — Inbox addon required or profile limit reached
- `404` — Account or recipient user not found (Reddit: PARTICIPANT_NOT_FOUND when the u/username does not exist)
- `422` — Recipient does not accept DMs from this account (X/Twitter), or does not accept private messages from you (Reddit)
- `429` — X API rate limit exceeded, or Reddit rate limit reached for this account

## Changes

- **2026-08-29** `e6f7a453bfb1` — 1 info
  - added the new optional request property `headerLocation`
- **2026-08-26** `b5344d376e5e` — 1 info
  - added the new optional request property `templateButtonParams`
- **2026-08-22** `19fd2b70854e` — 2 info
  - added the media type `application/json` for the response with the status `404`
  - added the media type `application/json` for the response with the status `429`
- **2026-08-18** `d0ab3b4eb0ae` — 1 info
  - added the new optional request property `linkPreview`

[Change history](https://skmtc.dev/zernio/apis/zernio-api/changes/v1/inbox/conversations/post.md)

---

[API](https://skmtc.dev/zernio/apis/zernio-api.md) · [All operations](https://skmtc.dev/zernio/apis/zernio-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/zernio/zernio-api/revisions/0caf79db3a97/schema)
