Source: ai-research/ghl-2026-05-01/support-solutions-articles-155000007340-conversations-api-add-inbound-message-with-contact-id.md

The Add Inbound Message endpoint records an inbound message into the correct contact conversation thread using only a Contact ID — no prior conversation lookup or creation required. It is the canonical way to ingest external messaging events (provider webhooks, third-party SMS/email, hybrid messaging integrations) back into the GHL inbox so agents see one unified thread per contact. Threading rules, idempotency, and retry behavior are first-class concerns; the endpoint is also backward-compatible with existing Conversation-ID-based flows.

Key Takeaways

  • Contact-ID-first design. Provide contactId + channel + content and the system handles thread association. If an open conversation on the same channel exists, the message is appended; otherwise a new conversation is created and conversationId is returned in the response.
  • Endpoint: POST /conversations/inbound-messages with Authorization: Bearer <token> and Content-Type: application/json. The conceptual reference doc lives at the HighLevel API Documentation - Send a new message.
  • Channel coverage. SMS/MMS, Email, WhatsApp, Messenger, Instagram, Web Chat. Each channel enforces its own size, format, and template constraints; validate before posting.
  • Disambiguation via endpoint hint. When a contact has multiple phones or emails for the same channel, include endpoint.phone or endpoint.email in the payload so the system threads to the correct address.
  • Idempotency is mandatory in practice. Always send an idempotencyKey; reuse the same key on retries to prevent duplicate messages on 5xx/timeout retries.
  • Backfill supported. Set metadata.timestamp to the original message time and the message orders correctly in the timeline — useful for historical imports.
  • Auth and scope. Use OAuth or API key per environment; confirm Conversations/Contacts scopes for the role and that the auth context resolves to the correct location/workspace.

Endpoint Reference

POST /conversations/inbound-messages
Authorization: Bearer <token>
Content-Type: application/json

Representative Request

{
  "contactId": "CONTACT_ID",
  "channel": "sms | whatsapp | email | messenger | instagram | webchat",
  "endpoint": {
    "phone": "+15551234567",
    "email": "user@example.com"
  },
  "content": {
    "text": "Hello from our provider",
    "attachments": [
      {
        "type": "image|file|video",
        "url": "https://example.com/file.jpg",
        "filename": "file.jpg",
        "sizeBytes": 123456
      }
    ]
  },
  "metadata": {
    "providerMessageId": "ext-abc-123",
    "externalThreadKey": "optional-correlation",
    "timestamp": "2026-02-04T10:15:30Z"
  },
  "idempotencyKey": "fd2d5f6f-5a9f-4b0a-8d68-0d5f6a1c9e5a"
}

Representative Response

{
  "messageId": "MSG_123",
  "conversationId": "CONV_987",
  "contactId": "CONTACT_ID",
  "channel": "sms",
  "direction": "inbound",
  "createdAt": "2026-02-04T10:15:31Z"
}

The source notes that field names are representative — production implementations should align with the latest developer docs.

Threading & Association Rules

  • Existing thread (same channel): message is appended to the contact’s open conversation on that channel.
  • No suitable thread: the system creates a new conversation and returns a fresh conversationId.
  • Multiple endpoints per contact: include endpoint.phone / endpoint.email to disambiguate.
  • Multi-location context: auth must target the correct location/workspace that owns the contact.
  • Archived/closed threads only: a new conversation is created.

Error Handling

StatusMeaning
400Invalid contactId, missing channel, malformed content, unsupported attachment type/size
401 / 403Auth failed or insufficient scope/role for the location
404contactId not found or not visible in your location context
409Idempotency key collision or duplicate submission detected
413Attachments exceed size limits
429Rate limit exceeded — back off and retry, respect Retry-After
5xxTransient — retry with the same idempotencyKey

Common Use Cases

  • Logging external SMS/email/voice into GHL. A third-party SMS or email provider sends a webhook; your integration POSTs that event back into GHL so the inbox shows the full conversation history.
  • Hybrid messaging integrations. Outbound messages go through your own provider (cost, deliverability, compliance), but inbound replies still need to land in the GHL agent inbox.
  • Historical backfill / migrations. Importing message history from another CRM with original timestamps preserved via metadata.timestamp.
  • Multi-channel routing. A single integration ingests messages across SMS, WhatsApp, Messenger, Instagram, and Email without per-channel branching logic.

Try It

  1. Confirm your OAuth token has Conversations + Contacts scopes for the target location.
  2. Resolve the recipient contactId upstream (lookup-by-phone or lookup-by-email).
  3. Build the payload with contactId, channel, content.text, and a UUID idempotencyKey.
  4. POST to /conversations/inbound-messages and capture the returned conversationId for downstream tracking.
  5. Add retry logic: on 429 honor Retry-After; on 5xx/timeout retry with the same idempotencyKey; on 409 treat as success (duplicate already recorded).
  6. Verify by opening the contact in the GHL inbox and confirming the message lands in the correct thread.