---
title: "Send a care chat message"
method: POST
path: "/care/chats/{chatId}/messages"
tags: ["Embedded Care"]
---

# Send a care chat message

`POST /care/chats/{chatId}/messages`

## Request

- Base URL: `https://api.lightspark.com/grid/2025-10-13`
- URL: `https://api.lightspark.com/grid/2025-10-13/care/chats/{chatId}/messages`
- Auth: HTTP basic

Run one turn of the cardholder's conversation with the support agent, or,
with an empty `message`, open the chat without saying anything. This is
the entry point to the whole surface: a partner who has never called any
other care endpoint starts here.

**Render `redactedCustomerMessage` if present, then `reply` if present.**
That one rule covers every call: an opening, a resume and a turn.
`redactedCustomerMessage` is what the cardholder typed with card numbers
and other sensitive digit strings masked. Render it, never your own copy of
what they typed, which would put those digits back on their screen and
into your logs.

**One turn, synchronously.** With a non-empty `message` the call holds the
connection until the agent has answered, and returns the answer in
`reply`. An `Idempotency-Key` header is required, and it is the only way
to recover an answer: nothing on this surface returns a transcript, so a
reply that was produced but not delivered is reachable only by retrying
under the same key, which returns the original reply, with the same `id`,
and never a `sessionToken`. **A replay requires the same session token its original
request required.** Replaying a turn that was sent while the chat could
reach the account needs a valid `X-Ls-Care-Session` for this chat; without
one it is refused with `401 CARE_SESSION_EXPIRED` and returns no reply. A
replay of a turn that ran before the cardholder verified, such as the turn
in which they sent their code, needs no token. A replay never returns a
`sessionToken`, even if the original response carried one, and it never
cancels a verification or sends a code. So a partner who lost the token
cannot replay an account turn: send the cardholder's next message as a new
message without a token, which verifies them again (the verification is
cancelled, a new code is sent, and the agent asks for it). A replay is
answered, under the same rule, even after the chat has ended. A retry sent
while the original turn is still running is a replay too: it waits for
that turn's answer rather than running a second turn. A turn that
failed does not pin its key: the same key may be sent again and will run a
fresh turn.

**An empty or absent `message` opens the chat and runs no turn.** On a
cardholder with no open chat it opens one, and `reply` carries the agent's
opening line, which is what a chat window renders before anybody has
typed. On a cardholder who already has one it resumes: the chat's id and
where verification stands come back, and `reply` does not, because an
opening line arriving mid-conversation reads as the agent introducing
itself a second time. `id` and `redactedCustomerMessage` are absent on
both, because nothing was typed. Either way no code is sent and
no `Idempotency-Key` is needed or consumed, so a widget in a reconnect
loop may still call it as often as it likes. The exception is an opening
addressed to a `CareChat:<id>` that has ended: it answers
`409 CARE_CHAT_ENDED`, and the recovery is to address the cardholder as
`Customer:<uuid>`, which opens a new chat.

**`chatEnded` says when to stop.** It is on every `200` body, true or
false, and describes the chat once the call finished: true means the chat
has ended and no further message will be accepted. The agent can end the
chat itself when the cardholder is done; on that turn `chatEnded` is true
and the closing line is in `reply`. It is the chat's state rather than
what this call did, so a replay of that turn also says true. When it is
true, stop sending to that chat.

**Verification happens in the conversation, and is never a `401`.** A
chat that cannot yet reach the account answers `200`, and the first
message with text texts the cardholder a code; later messages on the same
chat send no second one. The agent's own sentence in `reply` asks for the
code, so a partner that only renders the conversation needs nothing else.
`verificationRequired` and `codeLength` are optional hints for an app that
wants a dedicated one-time-code input of the right length, marked as a
one-time code so iOS and Android offer to fill it from the SMS. The app
still sends the code as an ordinary message.

**The cardholder completes verification by sending the code as an
ordinary message, and that turn's response carries the session.** When the
code is correct, the chat comes out of that turn able to reach the
account, and the body of that turn carries `sessionToken` and `expiresAt`.
No other turn carries them: a session is issued per verification, not per
message. Store the token and send it from the next call onwards. A code
that does not match is answered in `reply`, not as an error, and the
cardholder can send it again.

**A cardholder whose code never arrived asks the agent for another one**,
in the conversation, and the agent sends a new code to the number on file.
There is no separate route for sending a code.

While the chat's verification is current, send the session token in the
`X-Ls-Care-Session` header on every message.

**A verification goes stale, and the agent then asks for a code again
rather than the call failing.** A chat reaches the cardholder's account
only while its verification is newer than the session lifetime, fifteen
minutes in the default deployment, which is the same window a session
token lives for, so in practice the chat and the token go stale together.
Past that window the next message **sent without the header** is treated
as an unverified one: a fresh code goes out, the body carries
`verificationRequired: true`, and the agent's own `reply` asks the
cardholder to send the code. The turn still runs and still answers.

**A lost or expired session token verifies again rather than being
refused.** A new message sent with no `X-Ls-Care-Session` header, or with a
token past its expiry, on a chat whose verification is still current is
treated the same way: the verification is cancelled before the turn runs,
so the turn cannot reach the account, and the call then proceeds exactly
as on a stale chat. A fresh code goes out, the agent asks the cardholder
for it, and the turn in which they send it returns the next
`sessionToken`. No special handling is needed: a client that lost its
token simply keeps sending messages.

Only a token that is malformed is refused with `CARE_SESSION_EXPIRED`, and
a genuine token minted for another chat or another cardholder with
`NOT_FOUND`, or for another platform with `CARE_SESSION_PLATFORM_MISMATCH`.
Those are a bug in the caller rather than a lost session, and nothing on
the chat changes. An empty message never reads the header at all, so a
widget still holding a dead token can resume with one and be told where
verification stands without texting anybody.

Everything in the response except `chatEnded` describes the chat as of
the moment the turn started. The agent can verify a cardholder during a
turn, so a chat that this very call verified still reads
`verificationRequired: true` here; decide that you hold a session by the
presence of `sessionToken`.

Requires a token with the `VIEW` and `MANAGE` permissions; `VIEW` alone
is not sufficient, because the agent can freeze, unfreeze and replace the
cardholder's cards.

## Headers

- `Idempotency-Key` string
- `X-Ls-Care-Session` string

## Request body

- CareChatMessageRequest: One turn of the cardholder's conversation, or, with `message` empty or absent, the call that opens or resumes the chat without saying anything. Unknown fields are rejected with `INVALID_INPUT`.
  - `message` string: What the cardholder typed, verbatim and unredacted. Send it exactly as they entered it: Grid masks card numbers and other sensitive digit strings and returns the result as `redactedCustomerMessage`, which is what your UI must render. Empty, absent, or whitespace-only opens the chat (or resumes an open one) and runs no turn: no agent reply to a message, no SMS, and no `Idempotency-Key` needed or consumed. A non-empty value runs exactly one turn synchronously and requires an `Idempotency-Key` header.
  - `locale` string: BCP 47 locale the agent should answer in. Read only by the call that opens the chat and ignored afterwards, because changing it later would rewrite the language earlier turns were answered in. Defaults to `en-US`.

## Response `200`

The turn ran, or the chat was opened or resumed. Which fields are present depends on which of those happened.

- CareChatMessageResponse: The result of one call to the message route. Which fields are present depends on what the call did: `chatId`, `chatEnded` and `verificationRequired` are always present; `codeLength` is added when verification is required and the length is known; a turn adds `id`, `redactedCustomerMessage` and `reply`, and the call that opened the chat adds `reply` alone, carrying the agent's opening line; and the turn in which the cardholder sent their verification code adds `sessionToken` and `expiresAt`. Render `redactedCustomerMessage` if present, then `reply` if present.
  - `chatId` string, required: The chat this call ran on. When the request addressed a cardholder (`Customer:<uuid>` in the path) this is the chat that was found or opened for them. Use it for every later call on this conversation: later messages and closing the chat.
  - `chatEnded` boolean, required: Whether the chat has ended as of this response, so no further message will be accepted. Present on every response, true or false, so a client reads a field rather than an absence. It is the chat's state, not whether this call ended it: on the turn where the agent ends the chat it is true and the closing line is in `reply`, and a replay of that turn after the chat ended is also true. When it is true, stop sending to this chat; to talk to the cardholder again, address them as `Customer:<uuid>`, which opens a new chat.
  - `verificationRequired` boolean, required: An optional hint: whether the cardholder still has to verify before the agent can reach their account. Present on every response, true or false. A partner can ignore it, because when a code is needed the agent asks for it in `reply`; it lets an app show a dedicated code input beside the chat. Either way, the cardholder sends the code as an ordinary message. An outstanding verification is never reported as a `401`. It describes the chat as of the moment the turn started, so the one turn in which the cardholder sent their code reports `verificationRequired: true` and carries `sessionToken` as well. Decide that you hold a session by the presence of `sessionToken`, never by this field going false.
  - `id` string: Id of the cardholder's message. Present only when a turn ran, so absent on an opening or a resume. A retry under the same `Idempotency-Key` returns the same `id`, so a client that has already rendered this id can skip the exchange instead of showing it twice.
  - `redactedCustomerMessage` string: What the cardholder typed, with card numbers and other sensitive digit strings masked, and **the string your app must render in the transcript**. Render this, never your own copy of what the cardholder typed: your copy would put a card number they pasted back on their screen and into your logs. Present only when a turn ran.
  - `reply` string: What the agent said on this call, to render as the assistant's message. Present whenever the agent said something: its answer when a turn ran, or its opening line on the call that opened the chat. Absent on a resume. An opening needs no `Idempotency-Key` and consumes none, even though it carries a `reply`. A retry under the same `Idempotency-Key` returns the same reply and the same `id`, provided it carries the session token its original request required: retrying a turn sent while the chat could reach the account needs a valid `X-Ls-Care-Session`, and without one is refused with `401 CARE_SESSION_EXPIRED`; a turn that ran before the cardholder verified can be retried without one. A retry never carries a `sessionToken`, even if the original response did. When verification is outstanding the agent's own sentence asks for the code, and when a code the cardholder sent did not match, this is where they are told so.
  - `codeLength` integer: An optional hint: the number of digits in the code, so an app can show a one-time-code input of the right length, marked as a one-time code so iOS and Android offer to fill it from the SMS. The app still sends the code as an ordinary message. Absent when the length is not known; leave the input unbounded rather than guessing, since a wrong length makes every correct code look like a typo.
  - `sessionToken` string: The session token for this chat, present only on the turn in which the cardholder verified themselves, and absent on every other turn. The cardholder completes verification by sending the SMS code as an ordinary message. When the code is correct, the chat comes out of that turn able to reach the account, and this field is how the partner receives the credential that turn earned. It is the only place a session token is ever issued. A later message sent without it cancels the verification and asks the cardholder for a new code. Send it as the `X-Ls-Care-Session` header from the next call onwards. It is returned once and never re-readable, it cannot be revoked, and it is bound to this platform, this chat and this cardholder. Never log it, never put it in a URL, and never store it past `expiresAt`. One session is issued per verification rather than per message, so an ordinary turn on a chat that is already verified carries no `sessionToken`. The one you are already holding is still the right one. A replay under the same `Idempotency-Key` never returns a `sessionToken`, even if the original response carried one, and a replay of a turn that required a session needs that session itself. So if the response carrying the token was lost, you cannot recover it or replay an account turn: send the cardholder's next message as a new message without a token. The verification is cancelled, a new code is sent, and the agent asks for it. A replay of the turn in which the cardholder sent their code needs no token and returns the original reply without one.
  - `expiresAt` string, date-time: ISO 8601 timestamp, in UTC, after which the `sessionToken` beside it no longer counts as a session: a message carrying it past this time is treated as carrying none, and the cardholder is asked to verify again. Present exactly when `sessionToken` is present. Read it rather than assuming a duration.

## Other responses

- `400`: Bad request. `INVALID_INPUT` when the `Idempotency-Key` header is missing on a message that carries text or exceeds 255 characters, when the body is not a JSON object or carries an unknown field, or when the path id is a `Customer:` id that is not well formed. `CARE_MESSAGE_REJECTED` when the cardholder's message was refused before any turn ran; `reason` is written to be shown to them.
- `401`: `UNAUTHORIZED` when the platform's Grid credential is missing or wrong. `CARE_SESSION_EXPIRED` when the `X-Ls-Care-Session` token presented is malformed, or when a replay under an `Idempotency-Key` lacks the valid session token its original request required; no reply is returned. Otherwise a missing or expired session token is never a `401`: a new message verifies the cardholder again inside a `200` carrying `verificationRequired: true`, as does an unverified or stale chat. `CARE_SESSION_EXPIRED` is also returned, rarely, when a verification lands on the chat at the same moment a message without a live token takes it away; send the message again.
- `403`: `FORBIDDEN` when the token lacks the `VIEW` or `MANAGE` permission, or embedded care is not enabled for this platform. `CARE_SESSION_PLATFORM_MISMATCH` when the session token was minted for another platform.
- `404`: `NOT_FOUND` when the path names a chat this platform does not own, or when the `X-Ls-Care-Session` token was minted for a different chat or for a different cardholder; a token that does not belong to the chat in the path is a chat-scoped fact and answers the same way a missing chat does. `CUSTOMER_NOT_FOUND` when the path names a cardholder this platform does not own. A resource that belongs to somebody else and one that never existed are the same answer.
- `409`: `CONFLICT` when the `Idempotency-Key` was already used for a different message or a different chat (use a new key), or when the chat has no cardholder bound to it. `CARE_CHAT_ENDED` when a message, or an opening, is sent to a chat that has ended, other than a replay of a turn that completed or is still running; open a new one by sending a message addressed to the cardholder as `Customer:<uuid>`, which finds no open chat and opens a fresh one.
- `422`: `CARE_NOT_CONFIGURED_FOR_PLATFORM` when a message addressed to a cardholder has to find or open their chat and the platform, though entitled to embedded care, has no care programme configured.
- `429`: `CARE_BUDGET_EXCEEDED` when this chat has reached its limit; opening a new chat for the cardholder does not clear it. `RATE_LIMITED` when the platform is calling this endpoint too often; back off for the interval in the `Retry-After` header.
- `500`: Internal service error.
- `502`: `CARE_TURN_FAILED` when the agent could not answer, and `CARE_VERIFICATION_UNAVAILABLE` when the code could not be sent. Both are transient and neither pins the key: retry with the same `Idempotency-Key`.
- `503`: `SERVICE_UNAVAILABLE` when embedded care is not available in this deployment, or the support agent is temporarily turned off.
- `504`: `CARE_TURN_TIMEOUT` when the turn is still running. The answer is not lost: retry with the same `Idempotency-Key` to collect it.

## Changes

> 115 revisions in range; 2 not diffed.

- **2026-10-17** `661b7d710fd2`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/lightsparkdev/apis/grid-api/changes/care/chats/:chatId/messages/post.md)

---

[API](https://skmtc.dev/lightsparkdev/apis/grid-api.md) - [All operations](https://skmtc.dev/lightsparkdev/apis/grid-api/llms.txt) - [OpenAPI document](https://skmtc.dev/lightsparkdev/apis/grid-api/revisions/7a1db46ec552?raw)
