From b4995858eb5d37f43611bdf258cb4a5e703d4433 Mon Sep 17 00:00:00 2001 From: kphurley7 Date: Sat, 12 Sep 2026 19:25:50 -0500 Subject: [PATCH 01/11] feat(care): document the embedded care chat endpoints Adds the partner-facing care chat surface to the spec: the message route that opens a chat and runs a turn, the verification resend, the code confirmation that issues a session token, and the route that ends a chat. The endpoints are implemented in lightsparkdev/webdev at sparkcore/sparkcore/grid/api_handlers/care_chats.py, with the service layer in sparkcore/sparkcore/care/partner_conversations.py. The four operations answer with `{code, reason}` rather than the `{status, code, message}` shape the Error4xx/Error5xx schemas declare, so they reference a CareError schema that says what the server actually sends. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TDh3ab6V4rrpVG4v8cjj1u --- .stainless/stainless.yml | 23 + mintlify/openapi.yaml | 800 ++++++++++++++++++ openapi.yaml | 800 ++++++++++++++++++ .../schemas/care/CareChatGreeting.yaml | 25 + .../schemas/care/CareChatMessageRequest.yaml | 29 + .../schemas/care/CareChatMessageResponse.yaml | 94 ++ .../schemas/care/CareChatSession.yaml | 44 + .../CareChatVerificationConfirmRequest.yaml | 16 + .../care/CareChatVerificationRequest.yaml | 21 + .../care/CareChatVerificationResponse.yaml | 31 + .../care/CareChatVerificationStatus.yaml | 26 + .../components/schemas/care/CareError.yaml | 72 ++ openapi/openapi.yaml | 75 ++ openapi/paths/care/care_chats_{chatId}.yaml | 78 ++ .../care/care_chats_{chatId}_messages.yaml | 238 ++++++ .../care_chats_{chatId}_verifications.yaml | 138 +++ ..._chats_{chatId}_verifications_confirm.yaml | 133 +++ 17 files changed, 2643 insertions(+) create mode 100644 openapi/components/schemas/care/CareChatGreeting.yaml create mode 100644 openapi/components/schemas/care/CareChatMessageRequest.yaml create mode 100644 openapi/components/schemas/care/CareChatMessageResponse.yaml create mode 100644 openapi/components/schemas/care/CareChatSession.yaml create mode 100644 openapi/components/schemas/care/CareChatVerificationConfirmRequest.yaml create mode 100644 openapi/components/schemas/care/CareChatVerificationRequest.yaml create mode 100644 openapi/components/schemas/care/CareChatVerificationResponse.yaml create mode 100644 openapi/components/schemas/care/CareChatVerificationStatus.yaml create mode 100644 openapi/components/schemas/care/CareError.yaml create mode 100644 openapi/paths/care/care_chats_{chatId}.yaml create mode 100644 openapi/paths/care/care_chats_{chatId}_messages.yaml create mode 100644 openapi/paths/care/care_chats_{chatId}_verifications.yaml create mode 100644 openapi/paths/care/care_chats_{chatId}_verifications_confirm.yaml diff --git a/.stainless/stainless.yml b/.stainless/stainless.yml index eb2440c15..2b98d6e06 100644 --- a/.stainless/stainless.yml +++ b/.stainless/stainless.yml @@ -682,6 +682,29 @@ resources: retrieve: get /cards/{id} update: patch /cards/{id} + care: + subresources: + chats: + methods: + send_message: post /care/chats/{chatId}/messages + delete: delete /care/chats/{chatId} + models: + care_chat_message_request: "#/components/schemas/CareChatMessageRequest" + care_chat_message_response: "#/components/schemas/CareChatMessageResponse" + care_chat_greeting: "#/components/schemas/CareChatGreeting" + care_chat_verification_status: "#/components/schemas/CareChatVerificationStatus" + care_error: "#/components/schemas/CareError" + subresources: + verifications: + methods: + send: post /care/chats/{chatId}/verifications + confirm: post /care/chats/{chatId}/verifications/confirm + models: + care_chat_verification_request: "#/components/schemas/CareChatVerificationRequest" + care_chat_verification_response: "#/components/schemas/CareChatVerificationResponse" + care_chat_verification_confirm_request: "#/components/schemas/CareChatVerificationConfirmRequest" + care_chat_session: "#/components/schemas/CareChatSession" + settings: # All generated integration tests that hit the prism mock http server are marked # as skipped. Removing this setting or setting it to false enables tests, but diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 9c7805a7c..97c8c3437 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -61,6 +61,73 @@ tags: description: Card management endpoints. Issue debit cards against an internal account, freeze / unfreeze, close, manage a card's funding source, and list card transactions. - name: Stablecoins description: Stablecoin issuance endpoints. Link provider accounts, register provider-created stablecoins, create direct mint/burn issuer operations, and track operation status. + - name: Embedded Care + description: | + A support chat between the cardholder and Lightspark's care agent, driven + from the partner's own app. The platform calls with its ordinary Grid + credential, but the person typing is the cardholder rather than the + caller, so reaching that cardholder's account takes a second proof: an + SMS code they answer, which the partner afterwards carries in the + `X-Ls-Care-Session` header. + + **The surface is four routes, and the happy path is two calls.** + + 1. **Open the chat.** `POST /care/chats/{chatId}/messages` with an empty + `message`, addressed to the cardholder as `Customer:`. Returns + the chat's id and the agent's `greeting`, which is what a chat window + renders before anybody has typed. Sends no SMS, runs no turn, bills + nothing, and is safe to call again — a widget that reloaded gets the + same chat back, this time without a greeting. + 2. **Send the cardholder's first message.** The same route with their + text and an `Idempotency-Key` header. One turn runs synchronously and + the agent's answer comes back in `reply`. On a chat that cannot yet + reach the account this call also texts the verification code and says + so in the same body: `verificationRequired: true`, with + `destinationMasked`, `alreadySent` and `codeLength` for the input you + put on screen. + 3. **Confirm the code.** + `POST /care/chats/{chatId}/verifications/confirm` returns a + `sessionToken` and its `expiresAt`. Send that token as the + `X-Ls-Care-Session` header on every message from then on. + 4. **Close the chat** with `DELETE /care/chats/{chatId}` when the + cardholder is done. + + `POST /care/chats/{chatId}/verifications` is the *Send another code* + button rather than a step in the flow: step 2 already sent the first + code. + + Step 1 is optional. A message addressed to a `Customer:` id opens that + cardholder's chat if they have none, so a partner whose cardholder has + just typed something spends one call rather than two. + + A cardholder who has not verified yet is never a `401`. That state is + reported inside a `200`, because the first message from every new + cardholder is unverified and a `401` there would be indistinguishable + from a bad credential. On these endpoints a `401` means the platform's + Grid credential is wrong, or a verified chat was driven without its + session token. + + Two things an integration has to get right: + + - **Render `displayMessage`, never your own copy of what was typed.** + Grid redacts the cardholder's text on the way in, and `displayMessage` + is that redacted echo. A UI that renders its own input box instead has + put a card number the cardholder pasted back on their screen unmasked, + and into every log the transcript reaches. + - **The session token is returned once and is never re-readable.** No + endpoint hands it back, there is no revocation, and it is short-lived + by design. Hold it for the life of the cardholder's session; a partner + that drops it has to send a new code and confirm it again. + + Some things are absent by construction, so no integration can come to + depend on them: nothing returns a transcript or an earlier turn's reply + (retrying under the original `Idempotency-Key` is how an answer that was + produced but not delivered is recovered), nothing sets a verification + state, and nothing changes the number a code is sent to. + + Errors from these endpoints carry `code` and `reason` only — see the + `CareError` schema, which differs from the `Error4xx` schemas used + elsewhere in this specification. paths: /config: get: @@ -10563,6 +10630,483 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /care/chats/{chatId}/messages: + parameters: + - name: chatId + in: path + description: 'Who this message is for. Two forms are accepted, and the id''s own prefix decides which: a `Customer:` addresses the cardholder and lands on their open chat, opening one if they have none; a `CareChat:` addresses that chat and no other. A bare uuid is read as a chat id. Addressing the cardholder is what lets a partner send their first message in one call instead of two.' + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + post: + summary: Send a care chat message + description: | + 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. + + **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 reads a transcript, so a + reply that was produced but not delivered is reachable only by retrying + under the same key, which returns the recorded answer with + `replayed: true`. 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 nothing.** On a + cardholder with no open chat it opens one and answers with `greeting`, 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 `greeting` does not, because + a greeting arriving mid-conversation reads as the agent introducing itself + a second time. Either way no code is sent, no `Idempotency-Key` is needed + or consumed, and nothing is billed — a widget in a reconnect loop may call + it as often as it likes. + + **Verification is signalled in this `200` body, never as a `401`.** Every + new cardholder's first message is unverified, so a `401` there would make + the normal case indistinguishable from a bad credential. A chat that + cannot yet reach the account answers `200` with + `verificationRequired: true`, and the first message with text is what + sends the code — `destinationMasked`, `alreadySent` and, where readable, + `codeLength` come back with it, and the agent's own sentence in `reply` + asks the cardholder for the code. Later messages on the same chat find the + code already outstanding and send nothing, reporting `alreadySent: true`. + Collect the code and post it to + `POST /care/chats/{chatId}/verifications/confirm`. + + **Render `displayMessage`, not your own copy of the input.** Grid redacts + the cardholder's text on the way in — a card number they pasted comes back + masked — and `displayMessage` is that redacted echo. A UI that echoes its + own input box instead has put an unmasked PAN in the transcript, on the + screen and in whatever logs the transcript reaches. + + Once the chat is verified every message must carry the session token from + `POST /care/chats/{chatId}/verifications/confirm` in the + `X-Ls-Care-Session` header. Without it the call is refused with + `CARE_SESSION_EXPIRED`: the chat's verified state never expires on its + own, so the session is what proves the cardholder is still at the keyboard. + + Everything in the response describes the chat as of the moment the turn + started, `verificationStatus` included. The agent can verify a cardholder + during a turn, so a chat that this very call verified still reads + unverified here and verified on the next call. + operationId: sendCareChatMessage + tags: + - Embedded Care + security: + - BasicAuth: [] + parameters: + - name: Idempotency-Key + in: header + required: false + description: 'A unique identifier for this turn, up to 255 characters. **Required whenever `message` is non-empty**; omitting it there is rejected with `INVALID_INPUT`. A retry carrying the same key returns the first call''s answer with `replayed: true`; aiming the same key at a different message, a different chat or a different cardholder is rejected with `409 CONFLICT`. An opening (empty `message`) neither needs nor consumes a key — one sent anyway is ignored, so that the partner''s first real message can still use it.' + schema: + type: string + maxLength: 255 + example: 550e8400-e29b-41d4-a716-446655440000 + - name: X-Ls-Care-Session + in: header + required: false + description: The `sessionToken` from `POST /care/chats/{chatId}/verifications/confirm`. Required on every message once the chat is verified, and refused with `CARE_SESSION_EXPIRED` when it is missing, malformed or past its expiry. Not read before verification, and not read on an opening — a widget that reloaded has lost its token, which is the case the opening exists for. + schema: + type: string + example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/CareChatMessageRequest' + examples: + openTheChat: + summary: Open or resume the cardholder's chat, saying nothing + value: + message: '' + firstMessage: + summary: The cardholder's first message, which also sends the code + value: + message: My card was declined at the grocery store + afterVerification: + summary: A later message, sent with the X-Ls-Care-Session header + value: + message: Can you tell me the last four digits on file? + responses: + '200': + description: The turn ran, or the chat was opened or resumed. Which fields are present depends on which of those happened. + content: + application/json: + schema: + $ref: '#/components/schemas/CareChatMessageResponse' + '400': + description: 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 at the content boundary — no turn ran, and `reason` is written to be shown to them. + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '401': + description: '`UNAUTHORIZED` when the platform''s Grid credential is missing or wrong. `CARE_SESSION_EXPIRED` when the chat is verified and the `X-Ls-Care-Session` header is missing, malformed or expired. An unverified cardholder is never a `401` — that is a `200` carrying `verificationRequired`.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '403': + description: '`FORBIDDEN` when embedded care is not enabled for this platform, or `CARE_SESSION_PLATFORM_MISMATCH` when the session token was minted for another platform.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '404': + description: '`NOT_FOUND` when the path names a chat this platform does not own, and `CUSTOMER_NOT_FOUND` when it names a cardholder this platform does not own. A resource that belongs to somebody else and one that never existed are the same answer.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '409': + description: '`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 the chat has been closed; open a new one by sending an empty message to the cardholder.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '422': + description: '`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.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '429': + description: '`CARE_BUDGET_EXCEEDED` when this chat has reached its limit. The chat is refused rather than replaced: opening a fresh one would be the limit cleared by asking again. `RATE_LIMITED` when the platform is calling this endpoint too often; back off for the interval in the `Retry-After` header.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '500': + description: Internal service error. + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '502': + description: '`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`.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '503': + description: '`SERVICE_UNAVAILABLE` when embedded care is not available in this deployment, or the support agent is temporarily turned off.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '504': + description: '`CARE_TURN_TIMEOUT` when the turn is still running. The answer is not lost — retry with the same `Idempotency-Key` to collect it.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + /care/chats/{chatId}/verifications: + parameters: + - name: chatId + in: path + description: The chat to send another code on, as returned in `chatId` by `POST /care/chats/{chatId}/messages`. + required: true + schema: + type: string + example: CareChat:019542f5-b3e7-1d02-0000-000000000030 + post: + summary: Resend a care chat verification code + description: | + Send the cardholder another verification code. + + **This is the resend, not the first send.** The cardholder's first message + already sent a code and said so in its own body; this is what the *Send + another code* button calls when they say it never arrived. A partner never + has to call it to start verification. + + With `resend` false — the default, and the same as sending no body — the + call asks for a code and is content to be told one is already outstanding: + the answer carries `alreadySent: true` and no new SMS goes out. That is a + success. With `resend` true a second code is sent. Ask the cardholder + before doing that: codes are sent from a pool of numbers, so the second + arrives from a different sender than the first, leaving them holding two + codes from two strangers and deciding which to read back. + + The destination is always the number already on file for the cardholder. + There is no way to nominate one, because a code sent to a number the + caller chose would prove nothing about the person holding the card. + + `202`, not `200`: the SMS is in flight, not delivered. + + A chat that is already verified is refused with `INVALID_INPUT` while its + verification is recent — there is nothing left to prove. Once that + verification has aged past the session lifetime the refusal lifts and a + fresh code can be sent. + operationId: resendCareChatVerification + tags: + - Embedded Care + security: + - BasicAuth: [] + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/CareChatVerificationRequest' + examples: + askForACode: + summary: Ask for a code, accepting the one already outstanding + value: + resend: false + deliberateResend: + summary: The cardholder says the first code never arrived + value: + resend: true + responses: + '202': + description: The code is on its way, or one was already outstanding and none was sent — `alreadySent` says which. + content: + application/json: + schema: + $ref: '#/components/schemas/CareChatVerificationResponse' + '400': + description: '`INVALID_INPUT` when the chat is already verified, when the body carries an unknown field, or when verification is not available for this chat at all — for example a cardholder with no phone number on file.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '401': + description: '`UNAUTHORIZED` — the platform''s Grid credential is missing or wrong.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '403': + description: '`FORBIDDEN` — embedded care is not enabled for this platform.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '404': + description: '`NOT_FOUND` — no such chat for this platform. A chat that belongs to another platform, one that never existed, and a malformed chat id are one answer.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '409': + description: '`CARE_CHAT_ENDED` when the chat has been closed, or `CONFLICT` when it has no cardholder bound to it.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '429': + description: '`CARE_BUDGET_EXCEEDED` when this chat has reached its limit, or `RATE_LIMITED` when the platform is calling this endpoint too often; back off for the interval in the `Retry-After` header.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '500': + description: Internal service error. + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '502': + description: '`CARE_VERIFICATION_UNAVAILABLE` — the SMS provider could not be reached. The factor is down, so retry rather than asking the cardholder for anything.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '503': + description: '`SERVICE_UNAVAILABLE` when embedded care is not available in this deployment, or the support agent is temporarily turned off.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + /care/chats/{chatId}/verifications/confirm: + parameters: + - name: chatId + in: path + description: The chat the code was sent on, as returned in `chatId` by `POST /care/chats/{chatId}/messages`. + required: true + schema: + type: string + example: CareChat:019542f5-b3e7-1d02-0000-000000000030 + post: + summary: Confirm a care chat verification code + description: | + Check the code the cardholder read back, and hand over the session that + every later message on this chat rides on. + + **The session token is returned here and never again.** No endpoint reads + it back, and there is no revocation — the short expiry is what makes that + acceptable. Keep it in the cardholder's own session on your side, send it + as the `X-Ls-Care-Session` header on every later message, and treat losing + it as re-verification: send a new code with + `POST /care/chats/{chatId}/verifications` and confirm it again. Read + `expiresAt` rather than assuming a lifetime. + + The token is bound to this platform, this chat and this cardholder. It + cannot drive another chat, and presenting it to another platform's chat is + refused with `CARE_SESSION_PLATFORM_MISMATCH`. + + A code that does not match is `CARE_VERIFICATION_FAILED` — ask the + cardholder again. A provider that could not be reached is + `CARE_VERIFICATION_UNAVAILABLE` — retry; nothing the cardholder does will + help. The two are kept apart deliberately, because they call for opposite + reactions. + + A chat whose verification is older than the session lifetime will not mint + a fresh session off it: that would be the platform issuing itself a + session for a cardholder who has not shown themselves in longer than a + session lasts. It is refused with `CARE_VERIFICATION_FAILED`, and the way + forward is a new code. + operationId: confirmCareChatVerification + tags: + - Embedded Care + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CareChatVerificationConfirmRequest' + example: + code: '482913' + responses: + '200': + description: The cardholder is verified on this chat, and the session token is in this body — it is not retrievable afterwards. + content: + application/json: + schema: + $ref: '#/components/schemas/CareChatSession' + '400': + description: '`CARE_VERIFICATION_FAILED` when the code did not match, or when the chat''s existing verification is too old to mint another session. `INVALID_INPUT` when the code is not of the shape the SMS carries, when the body is missing or carries an unknown field, or when verification is not available for this chat.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '401': + description: '`UNAUTHORIZED` — the platform''s Grid credential is missing or wrong.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '403': + description: '`FORBIDDEN` — embedded care is not enabled for this platform.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '404': + description: '`NOT_FOUND` — no such chat for this platform. A chat that belongs to another platform, one that never existed, and a malformed chat id are one answer.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '409': + description: '`CARE_CHAT_ENDED` when the chat has been closed, or `CONFLICT` when it has no cardholder bound to it.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '429': + description: '`CARE_BUDGET_EXCEEDED` when this chat has reached its limit, or `RATE_LIMITED` when the platform is calling this endpoint too often; back off for the interval in the `Retry-After` header.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '500': + description: Internal service error. + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '502': + description: '`CARE_VERIFICATION_UNAVAILABLE` — the SMS provider could not be reached, so the code could not be checked. Retry; the cardholder has done nothing wrong.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '503': + description: '`SERVICE_UNAVAILABLE` when embedded care is not available in this deployment, or the support agent is temporarily turned off.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + /care/chats/{chatId}: + parameters: + - name: chatId + in: path + description: The chat to close, as returned in `chatId` by `POST /care/chats/{chatId}/messages`. + required: true + schema: + type: string + example: CareChat:019542f5-b3e7-1d02-0000-000000000030 + delete: + summary: End a care chat + description: | + Close a chat the cardholder has finished with or navigated away from. The + conversation stops accepting messages; sending one afterwards is refused + with `CARE_CHAT_ENDED`, and the cardholder starts a new chat by sending + another message. + + No session token is required, even on a verified chat. Ending a chat is + the one thing the second factor is exempt from: it reaches nothing but the + chat's own closing timestamp, and requiring a live session would mean a + partner whose cardholder walked away could not close the chat without + texting them another code first. Ownership of the chat by the calling + platform is the whole bound. + + Closing a chat that is already closed is not an error — that is the state + the caller asked for. + operationId: endCareChat + tags: + - Embedded Care + security: + - BasicAuth: [] + responses: + '204': + description: The chat is closed. No content. + '401': + description: '`UNAUTHORIZED` — the platform''s Grid credential is missing or wrong.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '403': + description: '`FORBIDDEN` — embedded care is not enabled for this platform.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '404': + description: '`NOT_FOUND` — no such chat for this platform. A chat that belongs to another platform, one that never existed, and a malformed chat id are one answer.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '429': + description: '`RATE_LIMITED` — the platform is calling this endpoint too often. Back off for the interval in the `Retry-After` header.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '500': + description: Internal service error. + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '503': + description: '`SERVICE_UNAVAILABLE` when embedded care is not available in this deployment, or the support agent is temporarily turned off.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' webhooks: agent-action: post: @@ -27312,6 +27856,262 @@ components: type: string description: Provider account id. Optional when the submitted credentials expose exactly one provider account. example: 2VcUIonJeVQzFoBuC7LdFT0dRe4 + CareChatMessageRequest: + type: object + description: 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`. + properties: + message: + type: string + description: |- + What the cardholder typed, verbatim and unredacted. Send it exactly as they entered it: Grid redacts it on the way in and returns the redacted form as `displayMessage`, 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 model call, no SMS, no `Idempotency-Key` consumed, nothing billed. A non-empty value runs exactly one turn synchronously and requires an `Idempotency-Key` header. + example: My card was declined at the grocery store + locale: + type: string + minLength: 2 + maxLength: 32 + description: 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`. + example: en-US + CareChatVerificationStatus: + type: string + enum: + - PENDING + - SENT + - VERIFIED + - TRUSTED + - FAILED + description: | + Where the cardholder's proof of presence stands on this chat, as of the + moment the call was received. A chat in `VERIFIED` or `TRUSTED` can reach + the cardholder's account, and every message on it must carry the + `X-Ls-Care-Session` header; a chat in any other status cannot, and the + agent answers general questions only. + + This field reports the chat as it was when the turn started. The agent can + verify a cardholder during a turn, so a chat that was verified by the turn + you just made still reads `SENT` in that response and `VERIFIED` on the next + call. + + | Status | Description | + |--------|-------------| + | `PENDING` | No code has been sent on this chat yet. This is where every new chat starts. | + | `SENT` | A code has been sent to the cardholder and is outstanding. | + | `VERIFIED` | The cardholder answered the code. Account-reaching tools are available and `POST /care/chats/{chatId}/verifications/confirm` has issued a session token. | + | `TRUSTED` | The cardholder is treated as present without an SMS code on this chat. Reserved for channels Grid can already attribute; a partner chat does not start here. | + | `FAILED` | The last code was answered incorrectly or expired. Send another code with `POST /care/chats/{chatId}/verifications`. | + CareChatGreeting: + type: object + description: 'The agent''s opening line, recorded as the chat''s first message. Returned only by the call that opened the chat, never on a resume: a greeting arriving mid-conversation reads as the agent introducing itself a second time. Absent when the agent produced no opening line.' + required: + - id + - content + - createdAt + properties: + id: + type: string + description: Id of the recorded greeting message. + example: CareMessage:019542f5-b3e7-1d02-0000-000000000031 + content: + type: string + description: The greeting text to render in the transcript, in the chat's locale. + example: Hi, I'm the support assistant. What can I help you with today? + createdAt: + type: string + format: date-time + description: ISO 8601 timestamp, in UTC, of when the greeting was recorded. + example: '2026-09-12T14:10:00.123456Z' + CareChatMessageResponse: + type: object + description: 'The result of one call to the message route. Which fields are present depends on what the call did: a turn adds `id`, `displayMessage`, `reply` and `replayed`; the call that opened the chat adds `greeting`; an outstanding verification adds `destinationMasked`, `alreadySent` and, where the service could be read, `codeLength`.' + required: + - chatId + - verificationStatus + - verificationRequired + properties: + chatId: + type: string + description: 'The chat this call ran on. When the request addressed a cardholder (`Customer:` in the path) this is the chat that was found or opened for them. Use it for every later call on this conversation: confirming the code, resending it, and closing the chat.' + example: CareChat:019542f5-b3e7-1d02-0000-000000000030 + verificationStatus: + $ref: '#/components/schemas/CareChatVerificationStatus' + verificationRequired: + type: boolean + description: 'Whether the cardholder still has to prove they are present before the agent can reach their account. Present on every response, true or false, so a client reads a field rather than an absence. When true, `destinationMasked` and `alreadySent` are present and your UI should collect the code and post it to `POST /care/chats/{chatId}/verifications/confirm`. A verification that is outstanding is never reported as a `401`: on this surface a `401` means the platform credential is wrong, or a verified chat was driven without its session token.' + example: true + id: + type: string + description: Id of the cardholder's recorded message. Present only when a turn ran, so absent on an opening or a resume. + example: CareMessage:019542f5-b3e7-1d02-0000-000000000032 + displayMessage: + type: string + description: The redacted echo of what the cardholder typed, and **the string your app must render in the transcript**. Grid masks card numbers and other sensitive digit strings here, so rendering your own copy of the input instead puts a PAN the cardholder pasted back on their screen and into your logs. Present only when a turn ran. + example: My card [redacted card number ending 1111] was declined + reply: + type: string + description: The agent's answer to this message, to render as the assistant's turn. Present only when a turn ran. When verification is outstanding the agent's own sentence asks for the code, so render it beside your code input rather than instead of it. + example: I can help with that. To look at your account I've texted you a code — what does it say? + replayed: + type: boolean + description: Whether this answer came from the record of an earlier call under the same `Idempotency-Key` rather than from a turn run now. Present only when a turn ran. There is no route that reads a transcript, so a retry under the original key is the only way to recover a reply that was produced but never delivered. + example: false + greeting: + $ref: '#/components/schemas/CareChatGreeting' + destinationMasked: + type: string + description: The masked phone number the code was sent to, for a UI to show as "we texted ...". Present whenever `verificationRequired` is true. The destination is the number already on file for the cardholder and cannot be nominated by the caller. + example: '***0001' + alreadySent: + type: boolean + description: 'Whether a code was already outstanding, so this call sent no new SMS. Present whenever `verificationRequired` is true. It reads against the call you made: on an opening, true means some earlier call left a code outstanding; on a message, true means an earlier message already sent one. Say "we already texted you" rather than implying a fresh send.' + example: false + codeLength: + type: integer + description: Number of digits in the code, read from the verification service's own configuration. Absent when it could not be read — leave the input unbounded in that case rather than guessing, since a guess that disagrees with the service makes every correct code look like a typo. + example: 6 + CareError: + type: object + description: | + The error body returned by every endpoint under **Embedded Care**. + + It carries `code` and `reason`, and nothing else. This differs from the + `Error4xx` / `Error5xx` schemas the rest of this specification uses, which + declare `status` and `message`: the care endpoints do not send those two + fields. Read the HTTP status from the response line and the human-readable + text from `reason`. + + Branch on `code`, never on `reason` — the text is written for a human + reading a log and may be reworded at any time. + required: + - code + - reason + properties: + code: + type: string + description: | + Machine-readable error code. Each endpoint's response descriptions say + which of these it can send at which status. + + | Error Code | HTTP | Description | + |------------|------|-------------| + | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `customerId` that is not a `Customer` id, a code of the wrong shape, or a chat that cannot be verified at all. | + | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused at the content boundary and no turn ran. `reason` carries the sentence to show them. | + | `CARE_VERIFICATION_FAILED` | 400 | The code did not match, or the chat's existing verification is too old to mint another session. Send a new code and confirm it. | + | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified — that is signalled in a `200` body. | + | `CARE_SESSION_EXPIRED` | 401 | The chat is verified, so it requires `X-Ls-Care-Session`, and the header was missing, malformed, or past its expiry. Send the cardholder a new code and confirm it. | + | `FORBIDDEN` | 403 | Embedded care is not enabled for this platform. | + | `CARE_SESSION_PLATFORM_MISMATCH` | 403 | The session token was minted for a different platform. Stop; retrying will not help. | + | `NOT_FOUND` | 404 | No such chat for this platform. A chat belonging to another platform, a chat that never existed, and a malformed chat id are one answer. | + | `CUSTOMER_NOT_FOUND` | 404 | No such cardholder for this platform. | + | `CONFLICT` | 409 | The `Idempotency-Key` was already used for a different message or a different chat — use a new key — or the chat has no cardholder bound to it. | + | `CARE_CHAT_ENDED` | 409 | The chat has been closed. Open a new one by sending an empty message to the cardholder. | + | `CARE_NOT_CONFIGURED_FOR_PLATFORM` | 422 | The platform is entitled to embedded care but has no care programme configured. | + | `CARE_BUDGET_EXCEEDED` | 429 | This chat has reached its limit. It is not replaced by a new one: a replacement would be the limit cleared by asking again. | + | `RATE_LIMITED` | 429 | The platform is calling this endpoint too often. Back off for the interval in the `Retry-After` response header. | + | `INTERNAL_ERROR` | 500 | Unexpected server error. | + | `CARE_TURN_FAILED` | 502 | The agent could not answer. Transient; the same `Idempotency-Key` may be sent again. | + | `CARE_VERIFICATION_UNAVAILABLE` | 502 | The SMS provider could not be reached. Distinct from a wrong code: the factor is down, so retry rather than asking the cardholder again. | + | `SERVICE_UNAVAILABLE` | 503 | Embedded care is not available in this deployment, or the support agent is temporarily turned off. | + | `CARE_TURN_TIMEOUT` | 504 | The turn is still running. Retry with the same `Idempotency-Key` to collect the answer. | + enum: + - INVALID_INPUT + - CARE_MESSAGE_REJECTED + - CARE_VERIFICATION_FAILED + - UNAUTHORIZED + - CARE_SESSION_EXPIRED + - FORBIDDEN + - CARE_SESSION_PLATFORM_MISMATCH + - NOT_FOUND + - CUSTOMER_NOT_FOUND + - CONFLICT + - CARE_CHAT_ENDED + - CARE_NOT_CONFIGURED_FOR_PLATFORM + - CARE_BUDGET_EXCEEDED + - RATE_LIMITED + - INTERNAL_ERROR + - CARE_TURN_FAILED + - CARE_VERIFICATION_UNAVAILABLE + - SERVICE_UNAVAILABLE + - CARE_TURN_TIMEOUT + example: CARE_SESSION_EXPIRED + reason: + type: string + description: Human-readable explanation, for a log or a support ticket. Not for branching, and not always safe to show a cardholder verbatim — the exception is `CARE_MESSAGE_REJECTED`, whose `reason` is written to be shown. + example: The care session has expired. Verify the cardholder again. + CareChatVerificationRequest: + type: object + description: 'Options for re-sending the cardholder''s verification code. The body may be omitted entirely, which is the same as `{"resend": false}`.' + properties: + resend: + type: boolean + default: false + description: |- + `false` asks for a code and is content to be told one is already outstanding: the response then carries `alreadySent: true` and no new SMS is sent. That is a success, not an error — the cardholder should read back the code they already have. + + `true` is the deliberate second code, for a cardholder who says the first never arrived. Ask before you send one: codes are sent from a pool of numbers, so a second code arrives from a different sender than the first and leaves the cardholder holding two codes from two strangers, deciding which to read back. + example: false + CareChatVerificationResponse: + type: object + description: 'Acknowledgement that a verification code is on its way to the cardholder. `202`, not `200`: the SMS is in flight, not delivered.' + required: + - status + - destinationMasked + - alreadySent + properties: + status: + type: string + enum: + - SENT + description: Always `SENT`. The chat's own state is reported as `verificationStatus` on the message route. + example: SENT + destinationMasked: + type: string + description: The masked phone number the code is going to, for a UI to show as "we texted ...". Always the number already on file for the cardholder; the caller cannot nominate a destination. + example: '***0001' + alreadySent: + type: boolean + description: 'True when a code was already outstanding and this call sent no new SMS. Show "we already texted you" rather than implying a fresh send. A request with `resend: true` sends a new code and returns false.' + example: true + CareChatVerificationConfirmRequest: + type: object + description: The code the cardholder read back from their SMS. + required: + - code + properties: + code: + type: string + minLength: 1 + maxLength: 32 + description: The numeric code from the SMS, exactly as the cardholder entered it. A code of the wrong shape is rejected with `INVALID_INPUT`; a well-formed code that the provider says is not the one it sent is rejected with `CARE_VERIFICATION_FAILED`, which is the error to retry by asking the cardholder again. + example: '482913' + CareChatSession: + type: object + description: Proof that the cardholder answered their code, as a signed token the partner sends back on every later message. + required: + - status + - sessionToken + - expiresAt + properties: + status: + type: string + enum: + - VERIFIED + description: Always `VERIFIED`; a code that did not match is an error, not a status. + example: VERIFIED + sessionToken: + type: string + description: |- + The value to send as the `X-Ls-Care-Session` header on every later message on this chat. + + **It is returned here and never again.** There is no route that reads it back, and there is no revocation: the short expiry is what makes that acceptable. Hold it in the cardholder's session on your side for as long as they are chatting, and treat losing it as re-verification — send a new code with `POST /care/chats/{chatId}/verifications` and confirm it again. Never log it, never put it in a URL, and never store it past `expiresAt`. + + The token is bound to this platform, this chat and this cardholder, so it cannot drive another chat and does not survive being handed to another platform. + example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl + expiresAt: + type: string + format: date-time + description: ISO 8601 timestamp, in UTC, after which the token is refused with `CARE_SESSION_EXPIRED`. Sessions are short-lived by design — fifteen minutes in the default deployment. Read this field rather than assuming a duration. + example: '2026-09-12T14:25:00Z' WebhookType: type: string enum: diff --git a/openapi.yaml b/openapi.yaml index 9c7805a7c..97c8c3437 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -61,6 +61,73 @@ tags: description: Card management endpoints. Issue debit cards against an internal account, freeze / unfreeze, close, manage a card's funding source, and list card transactions. - name: Stablecoins description: Stablecoin issuance endpoints. Link provider accounts, register provider-created stablecoins, create direct mint/burn issuer operations, and track operation status. + - name: Embedded Care + description: | + A support chat between the cardholder and Lightspark's care agent, driven + from the partner's own app. The platform calls with its ordinary Grid + credential, but the person typing is the cardholder rather than the + caller, so reaching that cardholder's account takes a second proof: an + SMS code they answer, which the partner afterwards carries in the + `X-Ls-Care-Session` header. + + **The surface is four routes, and the happy path is two calls.** + + 1. **Open the chat.** `POST /care/chats/{chatId}/messages` with an empty + `message`, addressed to the cardholder as `Customer:`. Returns + the chat's id and the agent's `greeting`, which is what a chat window + renders before anybody has typed. Sends no SMS, runs no turn, bills + nothing, and is safe to call again — a widget that reloaded gets the + same chat back, this time without a greeting. + 2. **Send the cardholder's first message.** The same route with their + text and an `Idempotency-Key` header. One turn runs synchronously and + the agent's answer comes back in `reply`. On a chat that cannot yet + reach the account this call also texts the verification code and says + so in the same body: `verificationRequired: true`, with + `destinationMasked`, `alreadySent` and `codeLength` for the input you + put on screen. + 3. **Confirm the code.** + `POST /care/chats/{chatId}/verifications/confirm` returns a + `sessionToken` and its `expiresAt`. Send that token as the + `X-Ls-Care-Session` header on every message from then on. + 4. **Close the chat** with `DELETE /care/chats/{chatId}` when the + cardholder is done. + + `POST /care/chats/{chatId}/verifications` is the *Send another code* + button rather than a step in the flow: step 2 already sent the first + code. + + Step 1 is optional. A message addressed to a `Customer:` id opens that + cardholder's chat if they have none, so a partner whose cardholder has + just typed something spends one call rather than two. + + A cardholder who has not verified yet is never a `401`. That state is + reported inside a `200`, because the first message from every new + cardholder is unverified and a `401` there would be indistinguishable + from a bad credential. On these endpoints a `401` means the platform's + Grid credential is wrong, or a verified chat was driven without its + session token. + + Two things an integration has to get right: + + - **Render `displayMessage`, never your own copy of what was typed.** + Grid redacts the cardholder's text on the way in, and `displayMessage` + is that redacted echo. A UI that renders its own input box instead has + put a card number the cardholder pasted back on their screen unmasked, + and into every log the transcript reaches. + - **The session token is returned once and is never re-readable.** No + endpoint hands it back, there is no revocation, and it is short-lived + by design. Hold it for the life of the cardholder's session; a partner + that drops it has to send a new code and confirm it again. + + Some things are absent by construction, so no integration can come to + depend on them: nothing returns a transcript or an earlier turn's reply + (retrying under the original `Idempotency-Key` is how an answer that was + produced but not delivered is recovered), nothing sets a verification + state, and nothing changes the number a code is sent to. + + Errors from these endpoints carry `code` and `reason` only — see the + `CareError` schema, which differs from the `Error4xx` schemas used + elsewhere in this specification. paths: /config: get: @@ -10563,6 +10630,483 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /care/chats/{chatId}/messages: + parameters: + - name: chatId + in: path + description: 'Who this message is for. Two forms are accepted, and the id''s own prefix decides which: a `Customer:` addresses the cardholder and lands on their open chat, opening one if they have none; a `CareChat:` addresses that chat and no other. A bare uuid is read as a chat id. Addressing the cardholder is what lets a partner send their first message in one call instead of two.' + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + post: + summary: Send a care chat message + description: | + 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. + + **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 reads a transcript, so a + reply that was produced but not delivered is reachable only by retrying + under the same key, which returns the recorded answer with + `replayed: true`. 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 nothing.** On a + cardholder with no open chat it opens one and answers with `greeting`, 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 `greeting` does not, because + a greeting arriving mid-conversation reads as the agent introducing itself + a second time. Either way no code is sent, no `Idempotency-Key` is needed + or consumed, and nothing is billed — a widget in a reconnect loop may call + it as often as it likes. + + **Verification is signalled in this `200` body, never as a `401`.** Every + new cardholder's first message is unverified, so a `401` there would make + the normal case indistinguishable from a bad credential. A chat that + cannot yet reach the account answers `200` with + `verificationRequired: true`, and the first message with text is what + sends the code — `destinationMasked`, `alreadySent` and, where readable, + `codeLength` come back with it, and the agent's own sentence in `reply` + asks the cardholder for the code. Later messages on the same chat find the + code already outstanding and send nothing, reporting `alreadySent: true`. + Collect the code and post it to + `POST /care/chats/{chatId}/verifications/confirm`. + + **Render `displayMessage`, not your own copy of the input.** Grid redacts + the cardholder's text on the way in — a card number they pasted comes back + masked — and `displayMessage` is that redacted echo. A UI that echoes its + own input box instead has put an unmasked PAN in the transcript, on the + screen and in whatever logs the transcript reaches. + + Once the chat is verified every message must carry the session token from + `POST /care/chats/{chatId}/verifications/confirm` in the + `X-Ls-Care-Session` header. Without it the call is refused with + `CARE_SESSION_EXPIRED`: the chat's verified state never expires on its + own, so the session is what proves the cardholder is still at the keyboard. + + Everything in the response describes the chat as of the moment the turn + started, `verificationStatus` included. The agent can verify a cardholder + during a turn, so a chat that this very call verified still reads + unverified here and verified on the next call. + operationId: sendCareChatMessage + tags: + - Embedded Care + security: + - BasicAuth: [] + parameters: + - name: Idempotency-Key + in: header + required: false + description: 'A unique identifier for this turn, up to 255 characters. **Required whenever `message` is non-empty**; omitting it there is rejected with `INVALID_INPUT`. A retry carrying the same key returns the first call''s answer with `replayed: true`; aiming the same key at a different message, a different chat or a different cardholder is rejected with `409 CONFLICT`. An opening (empty `message`) neither needs nor consumes a key — one sent anyway is ignored, so that the partner''s first real message can still use it.' + schema: + type: string + maxLength: 255 + example: 550e8400-e29b-41d4-a716-446655440000 + - name: X-Ls-Care-Session + in: header + required: false + description: The `sessionToken` from `POST /care/chats/{chatId}/verifications/confirm`. Required on every message once the chat is verified, and refused with `CARE_SESSION_EXPIRED` when it is missing, malformed or past its expiry. Not read before verification, and not read on an opening — a widget that reloaded has lost its token, which is the case the opening exists for. + schema: + type: string + example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/CareChatMessageRequest' + examples: + openTheChat: + summary: Open or resume the cardholder's chat, saying nothing + value: + message: '' + firstMessage: + summary: The cardholder's first message, which also sends the code + value: + message: My card was declined at the grocery store + afterVerification: + summary: A later message, sent with the X-Ls-Care-Session header + value: + message: Can you tell me the last four digits on file? + responses: + '200': + description: The turn ran, or the chat was opened or resumed. Which fields are present depends on which of those happened. + content: + application/json: + schema: + $ref: '#/components/schemas/CareChatMessageResponse' + '400': + description: 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 at the content boundary — no turn ran, and `reason` is written to be shown to them. + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '401': + description: '`UNAUTHORIZED` when the platform''s Grid credential is missing or wrong. `CARE_SESSION_EXPIRED` when the chat is verified and the `X-Ls-Care-Session` header is missing, malformed or expired. An unverified cardholder is never a `401` — that is a `200` carrying `verificationRequired`.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '403': + description: '`FORBIDDEN` when embedded care is not enabled for this platform, or `CARE_SESSION_PLATFORM_MISMATCH` when the session token was minted for another platform.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '404': + description: '`NOT_FOUND` when the path names a chat this platform does not own, and `CUSTOMER_NOT_FOUND` when it names a cardholder this platform does not own. A resource that belongs to somebody else and one that never existed are the same answer.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '409': + description: '`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 the chat has been closed; open a new one by sending an empty message to the cardholder.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '422': + description: '`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.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '429': + description: '`CARE_BUDGET_EXCEEDED` when this chat has reached its limit. The chat is refused rather than replaced: opening a fresh one would be the limit cleared by asking again. `RATE_LIMITED` when the platform is calling this endpoint too often; back off for the interval in the `Retry-After` header.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '500': + description: Internal service error. + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '502': + description: '`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`.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '503': + description: '`SERVICE_UNAVAILABLE` when embedded care is not available in this deployment, or the support agent is temporarily turned off.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '504': + description: '`CARE_TURN_TIMEOUT` when the turn is still running. The answer is not lost — retry with the same `Idempotency-Key` to collect it.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + /care/chats/{chatId}/verifications: + parameters: + - name: chatId + in: path + description: The chat to send another code on, as returned in `chatId` by `POST /care/chats/{chatId}/messages`. + required: true + schema: + type: string + example: CareChat:019542f5-b3e7-1d02-0000-000000000030 + post: + summary: Resend a care chat verification code + description: | + Send the cardholder another verification code. + + **This is the resend, not the first send.** The cardholder's first message + already sent a code and said so in its own body; this is what the *Send + another code* button calls when they say it never arrived. A partner never + has to call it to start verification. + + With `resend` false — the default, and the same as sending no body — the + call asks for a code and is content to be told one is already outstanding: + the answer carries `alreadySent: true` and no new SMS goes out. That is a + success. With `resend` true a second code is sent. Ask the cardholder + before doing that: codes are sent from a pool of numbers, so the second + arrives from a different sender than the first, leaving them holding two + codes from two strangers and deciding which to read back. + + The destination is always the number already on file for the cardholder. + There is no way to nominate one, because a code sent to a number the + caller chose would prove nothing about the person holding the card. + + `202`, not `200`: the SMS is in flight, not delivered. + + A chat that is already verified is refused with `INVALID_INPUT` while its + verification is recent — there is nothing left to prove. Once that + verification has aged past the session lifetime the refusal lifts and a + fresh code can be sent. + operationId: resendCareChatVerification + tags: + - Embedded Care + security: + - BasicAuth: [] + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/CareChatVerificationRequest' + examples: + askForACode: + summary: Ask for a code, accepting the one already outstanding + value: + resend: false + deliberateResend: + summary: The cardholder says the first code never arrived + value: + resend: true + responses: + '202': + description: The code is on its way, or one was already outstanding and none was sent — `alreadySent` says which. + content: + application/json: + schema: + $ref: '#/components/schemas/CareChatVerificationResponse' + '400': + description: '`INVALID_INPUT` when the chat is already verified, when the body carries an unknown field, or when verification is not available for this chat at all — for example a cardholder with no phone number on file.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '401': + description: '`UNAUTHORIZED` — the platform''s Grid credential is missing or wrong.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '403': + description: '`FORBIDDEN` — embedded care is not enabled for this platform.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '404': + description: '`NOT_FOUND` — no such chat for this platform. A chat that belongs to another platform, one that never existed, and a malformed chat id are one answer.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '409': + description: '`CARE_CHAT_ENDED` when the chat has been closed, or `CONFLICT` when it has no cardholder bound to it.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '429': + description: '`CARE_BUDGET_EXCEEDED` when this chat has reached its limit, or `RATE_LIMITED` when the platform is calling this endpoint too often; back off for the interval in the `Retry-After` header.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '500': + description: Internal service error. + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '502': + description: '`CARE_VERIFICATION_UNAVAILABLE` — the SMS provider could not be reached. The factor is down, so retry rather than asking the cardholder for anything.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '503': + description: '`SERVICE_UNAVAILABLE` when embedded care is not available in this deployment, or the support agent is temporarily turned off.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + /care/chats/{chatId}/verifications/confirm: + parameters: + - name: chatId + in: path + description: The chat the code was sent on, as returned in `chatId` by `POST /care/chats/{chatId}/messages`. + required: true + schema: + type: string + example: CareChat:019542f5-b3e7-1d02-0000-000000000030 + post: + summary: Confirm a care chat verification code + description: | + Check the code the cardholder read back, and hand over the session that + every later message on this chat rides on. + + **The session token is returned here and never again.** No endpoint reads + it back, and there is no revocation — the short expiry is what makes that + acceptable. Keep it in the cardholder's own session on your side, send it + as the `X-Ls-Care-Session` header on every later message, and treat losing + it as re-verification: send a new code with + `POST /care/chats/{chatId}/verifications` and confirm it again. Read + `expiresAt` rather than assuming a lifetime. + + The token is bound to this platform, this chat and this cardholder. It + cannot drive another chat, and presenting it to another platform's chat is + refused with `CARE_SESSION_PLATFORM_MISMATCH`. + + A code that does not match is `CARE_VERIFICATION_FAILED` — ask the + cardholder again. A provider that could not be reached is + `CARE_VERIFICATION_UNAVAILABLE` — retry; nothing the cardholder does will + help. The two are kept apart deliberately, because they call for opposite + reactions. + + A chat whose verification is older than the session lifetime will not mint + a fresh session off it: that would be the platform issuing itself a + session for a cardholder who has not shown themselves in longer than a + session lasts. It is refused with `CARE_VERIFICATION_FAILED`, and the way + forward is a new code. + operationId: confirmCareChatVerification + tags: + - Embedded Care + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CareChatVerificationConfirmRequest' + example: + code: '482913' + responses: + '200': + description: The cardholder is verified on this chat, and the session token is in this body — it is not retrievable afterwards. + content: + application/json: + schema: + $ref: '#/components/schemas/CareChatSession' + '400': + description: '`CARE_VERIFICATION_FAILED` when the code did not match, or when the chat''s existing verification is too old to mint another session. `INVALID_INPUT` when the code is not of the shape the SMS carries, when the body is missing or carries an unknown field, or when verification is not available for this chat.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '401': + description: '`UNAUTHORIZED` — the platform''s Grid credential is missing or wrong.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '403': + description: '`FORBIDDEN` — embedded care is not enabled for this platform.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '404': + description: '`NOT_FOUND` — no such chat for this platform. A chat that belongs to another platform, one that never existed, and a malformed chat id are one answer.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '409': + description: '`CARE_CHAT_ENDED` when the chat has been closed, or `CONFLICT` when it has no cardholder bound to it.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '429': + description: '`CARE_BUDGET_EXCEEDED` when this chat has reached its limit, or `RATE_LIMITED` when the platform is calling this endpoint too often; back off for the interval in the `Retry-After` header.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '500': + description: Internal service error. + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '502': + description: '`CARE_VERIFICATION_UNAVAILABLE` — the SMS provider could not be reached, so the code could not be checked. Retry; the cardholder has done nothing wrong.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '503': + description: '`SERVICE_UNAVAILABLE` when embedded care is not available in this deployment, or the support agent is temporarily turned off.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + /care/chats/{chatId}: + parameters: + - name: chatId + in: path + description: The chat to close, as returned in `chatId` by `POST /care/chats/{chatId}/messages`. + required: true + schema: + type: string + example: CareChat:019542f5-b3e7-1d02-0000-000000000030 + delete: + summary: End a care chat + description: | + Close a chat the cardholder has finished with or navigated away from. The + conversation stops accepting messages; sending one afterwards is refused + with `CARE_CHAT_ENDED`, and the cardholder starts a new chat by sending + another message. + + No session token is required, even on a verified chat. Ending a chat is + the one thing the second factor is exempt from: it reaches nothing but the + chat's own closing timestamp, and requiring a live session would mean a + partner whose cardholder walked away could not close the chat without + texting them another code first. Ownership of the chat by the calling + platform is the whole bound. + + Closing a chat that is already closed is not an error — that is the state + the caller asked for. + operationId: endCareChat + tags: + - Embedded Care + security: + - BasicAuth: [] + responses: + '204': + description: The chat is closed. No content. + '401': + description: '`UNAUTHORIZED` — the platform''s Grid credential is missing or wrong.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '403': + description: '`FORBIDDEN` — embedded care is not enabled for this platform.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '404': + description: '`NOT_FOUND` — no such chat for this platform. A chat that belongs to another platform, one that never existed, and a malformed chat id are one answer.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '429': + description: '`RATE_LIMITED` — the platform is calling this endpoint too often. Back off for the interval in the `Retry-After` header.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '500': + description: Internal service error. + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' + '503': + description: '`SERVICE_UNAVAILABLE` when embedded care is not available in this deployment, or the support agent is temporarily turned off.' + content: + application/json: + schema: + $ref: '#/components/schemas/CareError' webhooks: agent-action: post: @@ -27312,6 +27856,262 @@ components: type: string description: Provider account id. Optional when the submitted credentials expose exactly one provider account. example: 2VcUIonJeVQzFoBuC7LdFT0dRe4 + CareChatMessageRequest: + type: object + description: 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`. + properties: + message: + type: string + description: |- + What the cardholder typed, verbatim and unredacted. Send it exactly as they entered it: Grid redacts it on the way in and returns the redacted form as `displayMessage`, 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 model call, no SMS, no `Idempotency-Key` consumed, nothing billed. A non-empty value runs exactly one turn synchronously and requires an `Idempotency-Key` header. + example: My card was declined at the grocery store + locale: + type: string + minLength: 2 + maxLength: 32 + description: 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`. + example: en-US + CareChatVerificationStatus: + type: string + enum: + - PENDING + - SENT + - VERIFIED + - TRUSTED + - FAILED + description: | + Where the cardholder's proof of presence stands on this chat, as of the + moment the call was received. A chat in `VERIFIED` or `TRUSTED` can reach + the cardholder's account, and every message on it must carry the + `X-Ls-Care-Session` header; a chat in any other status cannot, and the + agent answers general questions only. + + This field reports the chat as it was when the turn started. The agent can + verify a cardholder during a turn, so a chat that was verified by the turn + you just made still reads `SENT` in that response and `VERIFIED` on the next + call. + + | Status | Description | + |--------|-------------| + | `PENDING` | No code has been sent on this chat yet. This is where every new chat starts. | + | `SENT` | A code has been sent to the cardholder and is outstanding. | + | `VERIFIED` | The cardholder answered the code. Account-reaching tools are available and `POST /care/chats/{chatId}/verifications/confirm` has issued a session token. | + | `TRUSTED` | The cardholder is treated as present without an SMS code on this chat. Reserved for channels Grid can already attribute; a partner chat does not start here. | + | `FAILED` | The last code was answered incorrectly or expired. Send another code with `POST /care/chats/{chatId}/verifications`. | + CareChatGreeting: + type: object + description: 'The agent''s opening line, recorded as the chat''s first message. Returned only by the call that opened the chat, never on a resume: a greeting arriving mid-conversation reads as the agent introducing itself a second time. Absent when the agent produced no opening line.' + required: + - id + - content + - createdAt + properties: + id: + type: string + description: Id of the recorded greeting message. + example: CareMessage:019542f5-b3e7-1d02-0000-000000000031 + content: + type: string + description: The greeting text to render in the transcript, in the chat's locale. + example: Hi, I'm the support assistant. What can I help you with today? + createdAt: + type: string + format: date-time + description: ISO 8601 timestamp, in UTC, of when the greeting was recorded. + example: '2026-09-12T14:10:00.123456Z' + CareChatMessageResponse: + type: object + description: 'The result of one call to the message route. Which fields are present depends on what the call did: a turn adds `id`, `displayMessage`, `reply` and `replayed`; the call that opened the chat adds `greeting`; an outstanding verification adds `destinationMasked`, `alreadySent` and, where the service could be read, `codeLength`.' + required: + - chatId + - verificationStatus + - verificationRequired + properties: + chatId: + type: string + description: 'The chat this call ran on. When the request addressed a cardholder (`Customer:` in the path) this is the chat that was found or opened for them. Use it for every later call on this conversation: confirming the code, resending it, and closing the chat.' + example: CareChat:019542f5-b3e7-1d02-0000-000000000030 + verificationStatus: + $ref: '#/components/schemas/CareChatVerificationStatus' + verificationRequired: + type: boolean + description: 'Whether the cardholder still has to prove they are present before the agent can reach their account. Present on every response, true or false, so a client reads a field rather than an absence. When true, `destinationMasked` and `alreadySent` are present and your UI should collect the code and post it to `POST /care/chats/{chatId}/verifications/confirm`. A verification that is outstanding is never reported as a `401`: on this surface a `401` means the platform credential is wrong, or a verified chat was driven without its session token.' + example: true + id: + type: string + description: Id of the cardholder's recorded message. Present only when a turn ran, so absent on an opening or a resume. + example: CareMessage:019542f5-b3e7-1d02-0000-000000000032 + displayMessage: + type: string + description: The redacted echo of what the cardholder typed, and **the string your app must render in the transcript**. Grid masks card numbers and other sensitive digit strings here, so rendering your own copy of the input instead puts a PAN the cardholder pasted back on their screen and into your logs. Present only when a turn ran. + example: My card [redacted card number ending 1111] was declined + reply: + type: string + description: The agent's answer to this message, to render as the assistant's turn. Present only when a turn ran. When verification is outstanding the agent's own sentence asks for the code, so render it beside your code input rather than instead of it. + example: I can help with that. To look at your account I've texted you a code — what does it say? + replayed: + type: boolean + description: Whether this answer came from the record of an earlier call under the same `Idempotency-Key` rather than from a turn run now. Present only when a turn ran. There is no route that reads a transcript, so a retry under the original key is the only way to recover a reply that was produced but never delivered. + example: false + greeting: + $ref: '#/components/schemas/CareChatGreeting' + destinationMasked: + type: string + description: The masked phone number the code was sent to, for a UI to show as "we texted ...". Present whenever `verificationRequired` is true. The destination is the number already on file for the cardholder and cannot be nominated by the caller. + example: '***0001' + alreadySent: + type: boolean + description: 'Whether a code was already outstanding, so this call sent no new SMS. Present whenever `verificationRequired` is true. It reads against the call you made: on an opening, true means some earlier call left a code outstanding; on a message, true means an earlier message already sent one. Say "we already texted you" rather than implying a fresh send.' + example: false + codeLength: + type: integer + description: Number of digits in the code, read from the verification service's own configuration. Absent when it could not be read — leave the input unbounded in that case rather than guessing, since a guess that disagrees with the service makes every correct code look like a typo. + example: 6 + CareError: + type: object + description: | + The error body returned by every endpoint under **Embedded Care**. + + It carries `code` and `reason`, and nothing else. This differs from the + `Error4xx` / `Error5xx` schemas the rest of this specification uses, which + declare `status` and `message`: the care endpoints do not send those two + fields. Read the HTTP status from the response line and the human-readable + text from `reason`. + + Branch on `code`, never on `reason` — the text is written for a human + reading a log and may be reworded at any time. + required: + - code + - reason + properties: + code: + type: string + description: | + Machine-readable error code. Each endpoint's response descriptions say + which of these it can send at which status. + + | Error Code | HTTP | Description | + |------------|------|-------------| + | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `customerId` that is not a `Customer` id, a code of the wrong shape, or a chat that cannot be verified at all. | + | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused at the content boundary and no turn ran. `reason` carries the sentence to show them. | + | `CARE_VERIFICATION_FAILED` | 400 | The code did not match, or the chat's existing verification is too old to mint another session. Send a new code and confirm it. | + | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified — that is signalled in a `200` body. | + | `CARE_SESSION_EXPIRED` | 401 | The chat is verified, so it requires `X-Ls-Care-Session`, and the header was missing, malformed, or past its expiry. Send the cardholder a new code and confirm it. | + | `FORBIDDEN` | 403 | Embedded care is not enabled for this platform. | + | `CARE_SESSION_PLATFORM_MISMATCH` | 403 | The session token was minted for a different platform. Stop; retrying will not help. | + | `NOT_FOUND` | 404 | No such chat for this platform. A chat belonging to another platform, a chat that never existed, and a malformed chat id are one answer. | + | `CUSTOMER_NOT_FOUND` | 404 | No such cardholder for this platform. | + | `CONFLICT` | 409 | The `Idempotency-Key` was already used for a different message or a different chat — use a new key — or the chat has no cardholder bound to it. | + | `CARE_CHAT_ENDED` | 409 | The chat has been closed. Open a new one by sending an empty message to the cardholder. | + | `CARE_NOT_CONFIGURED_FOR_PLATFORM` | 422 | The platform is entitled to embedded care but has no care programme configured. | + | `CARE_BUDGET_EXCEEDED` | 429 | This chat has reached its limit. It is not replaced by a new one: a replacement would be the limit cleared by asking again. | + | `RATE_LIMITED` | 429 | The platform is calling this endpoint too often. Back off for the interval in the `Retry-After` response header. | + | `INTERNAL_ERROR` | 500 | Unexpected server error. | + | `CARE_TURN_FAILED` | 502 | The agent could not answer. Transient; the same `Idempotency-Key` may be sent again. | + | `CARE_VERIFICATION_UNAVAILABLE` | 502 | The SMS provider could not be reached. Distinct from a wrong code: the factor is down, so retry rather than asking the cardholder again. | + | `SERVICE_UNAVAILABLE` | 503 | Embedded care is not available in this deployment, or the support agent is temporarily turned off. | + | `CARE_TURN_TIMEOUT` | 504 | The turn is still running. Retry with the same `Idempotency-Key` to collect the answer. | + enum: + - INVALID_INPUT + - CARE_MESSAGE_REJECTED + - CARE_VERIFICATION_FAILED + - UNAUTHORIZED + - CARE_SESSION_EXPIRED + - FORBIDDEN + - CARE_SESSION_PLATFORM_MISMATCH + - NOT_FOUND + - CUSTOMER_NOT_FOUND + - CONFLICT + - CARE_CHAT_ENDED + - CARE_NOT_CONFIGURED_FOR_PLATFORM + - CARE_BUDGET_EXCEEDED + - RATE_LIMITED + - INTERNAL_ERROR + - CARE_TURN_FAILED + - CARE_VERIFICATION_UNAVAILABLE + - SERVICE_UNAVAILABLE + - CARE_TURN_TIMEOUT + example: CARE_SESSION_EXPIRED + reason: + type: string + description: Human-readable explanation, for a log or a support ticket. Not for branching, and not always safe to show a cardholder verbatim — the exception is `CARE_MESSAGE_REJECTED`, whose `reason` is written to be shown. + example: The care session has expired. Verify the cardholder again. + CareChatVerificationRequest: + type: object + description: 'Options for re-sending the cardholder''s verification code. The body may be omitted entirely, which is the same as `{"resend": false}`.' + properties: + resend: + type: boolean + default: false + description: |- + `false` asks for a code and is content to be told one is already outstanding: the response then carries `alreadySent: true` and no new SMS is sent. That is a success, not an error — the cardholder should read back the code they already have. + + `true` is the deliberate second code, for a cardholder who says the first never arrived. Ask before you send one: codes are sent from a pool of numbers, so a second code arrives from a different sender than the first and leaves the cardholder holding two codes from two strangers, deciding which to read back. + example: false + CareChatVerificationResponse: + type: object + description: 'Acknowledgement that a verification code is on its way to the cardholder. `202`, not `200`: the SMS is in flight, not delivered.' + required: + - status + - destinationMasked + - alreadySent + properties: + status: + type: string + enum: + - SENT + description: Always `SENT`. The chat's own state is reported as `verificationStatus` on the message route. + example: SENT + destinationMasked: + type: string + description: The masked phone number the code is going to, for a UI to show as "we texted ...". Always the number already on file for the cardholder; the caller cannot nominate a destination. + example: '***0001' + alreadySent: + type: boolean + description: 'True when a code was already outstanding and this call sent no new SMS. Show "we already texted you" rather than implying a fresh send. A request with `resend: true` sends a new code and returns false.' + example: true + CareChatVerificationConfirmRequest: + type: object + description: The code the cardholder read back from their SMS. + required: + - code + properties: + code: + type: string + minLength: 1 + maxLength: 32 + description: The numeric code from the SMS, exactly as the cardholder entered it. A code of the wrong shape is rejected with `INVALID_INPUT`; a well-formed code that the provider says is not the one it sent is rejected with `CARE_VERIFICATION_FAILED`, which is the error to retry by asking the cardholder again. + example: '482913' + CareChatSession: + type: object + description: Proof that the cardholder answered their code, as a signed token the partner sends back on every later message. + required: + - status + - sessionToken + - expiresAt + properties: + status: + type: string + enum: + - VERIFIED + description: Always `VERIFIED`; a code that did not match is an error, not a status. + example: VERIFIED + sessionToken: + type: string + description: |- + The value to send as the `X-Ls-Care-Session` header on every later message on this chat. + + **It is returned here and never again.** There is no route that reads it back, and there is no revocation: the short expiry is what makes that acceptable. Hold it in the cardholder's session on your side for as long as they are chatting, and treat losing it as re-verification — send a new code with `POST /care/chats/{chatId}/verifications` and confirm it again. Never log it, never put it in a URL, and never store it past `expiresAt`. + + The token is bound to this platform, this chat and this cardholder, so it cannot drive another chat and does not survive being handed to another platform. + example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl + expiresAt: + type: string + format: date-time + description: ISO 8601 timestamp, in UTC, after which the token is refused with `CARE_SESSION_EXPIRED`. Sessions are short-lived by design — fifteen minutes in the default deployment. Read this field rather than assuming a duration. + example: '2026-09-12T14:25:00Z' WebhookType: type: string enum: diff --git a/openapi/components/schemas/care/CareChatGreeting.yaml b/openapi/components/schemas/care/CareChatGreeting.yaml new file mode 100644 index 000000000..332744acb --- /dev/null +++ b/openapi/components/schemas/care/CareChatGreeting.yaml @@ -0,0 +1,25 @@ +type: object +description: >- + The agent's opening line, recorded as the chat's first message. Returned + only by the call that opened the chat, never on a resume: a greeting + arriving mid-conversation reads as the agent introducing itself a second + time. Absent when the agent produced no opening line. +required: + - id + - content + - createdAt +properties: + id: + type: string + description: Id of the recorded greeting message. + example: CareMessage:019542f5-b3e7-1d02-0000-000000000031 + content: + type: string + description: >- + The greeting text to render in the transcript, in the chat's locale. + example: Hi, I'm the support assistant. What can I help you with today? + createdAt: + type: string + format: date-time + description: ISO 8601 timestamp, in UTC, of when the greeting was recorded. + example: '2026-09-12T14:10:00.123456Z' diff --git a/openapi/components/schemas/care/CareChatMessageRequest.yaml b/openapi/components/schemas/care/CareChatMessageRequest.yaml new file mode 100644 index 000000000..8c54a33dc --- /dev/null +++ b/openapi/components/schemas/care/CareChatMessageRequest.yaml @@ -0,0 +1,29 @@ +type: object +description: >- + 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`. +properties: + message: + type: string + description: >- + What the cardholder typed, verbatim and unredacted. Send it exactly as + they entered it: Grid redacts it on the way in and returns the redacted + form as `displayMessage`, 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 model call, no SMS, no `Idempotency-Key` + consumed, nothing billed. A non-empty value runs exactly one turn + synchronously and requires an `Idempotency-Key` header. + example: My card was declined at the grocery store + locale: + type: string + minLength: 2 + maxLength: 32 + description: >- + 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`. + example: en-US diff --git a/openapi/components/schemas/care/CareChatMessageResponse.yaml b/openapi/components/schemas/care/CareChatMessageResponse.yaml new file mode 100644 index 000000000..0b0373810 --- /dev/null +++ b/openapi/components/schemas/care/CareChatMessageResponse.yaml @@ -0,0 +1,94 @@ +type: object +description: >- + The result of one call to the message route. Which fields are present + depends on what the call did: a turn adds `id`, `displayMessage`, `reply` + and `replayed`; the call that opened the chat adds `greeting`; an + outstanding verification adds `destinationMasked`, `alreadySent` and, + where the service could be read, `codeLength`. +required: + - chatId + - verificationStatus + - verificationRequired +properties: + chatId: + type: string + description: >- + The chat this call ran on. When the request addressed a cardholder + (`Customer:` in the path) this is the chat that was found or + opened for them. Use it for every later call on this conversation: + confirming the code, resending it, and closing the chat. + example: CareChat:019542f5-b3e7-1d02-0000-000000000030 + verificationStatus: + $ref: ./CareChatVerificationStatus.yaml + verificationRequired: + type: boolean + description: >- + Whether the cardholder still has to prove they are present before the + agent can reach their account. Present on every response, true or + false, so a client reads a field rather than an absence. When true, + `destinationMasked` and `alreadySent` are present and your UI should + collect the code and post it to + `POST /care/chats/{chatId}/verifications/confirm`. A verification that + is outstanding is never reported as a `401`: on this surface a `401` + means the platform credential is wrong, or a verified chat was driven + without its session token. + example: true + id: + type: string + description: >- + Id of the cardholder's recorded message. Present only when a turn ran, + so absent on an opening or a resume. + example: CareMessage:019542f5-b3e7-1d02-0000-000000000032 + displayMessage: + type: string + description: >- + The redacted echo of what the cardholder typed, and **the string your + app must render in the transcript**. Grid masks card numbers and other + sensitive digit strings here, so rendering your own copy of the input + instead puts a PAN the cardholder pasted back on their screen and into + your logs. Present only when a turn ran. + example: My card [redacted card number ending 1111] was declined + reply: + type: string + description: >- + The agent's answer to this message, to render as the assistant's turn. + Present only when a turn ran. When verification is outstanding the + agent's own sentence asks for the code, so render it beside your code + input rather than instead of it. + example: I can help with that. To look at your account I've texted you a code — what does it say? + replayed: + type: boolean + description: >- + Whether this answer came from the record of an earlier call under the + same `Idempotency-Key` rather than from a turn run now. Present only + when a turn ran. There is no route that reads a transcript, so a retry + under the original key is the only way to recover a reply that was + produced but never delivered. + example: false + greeting: + $ref: ./CareChatGreeting.yaml + destinationMasked: + type: string + description: >- + The masked phone number the code was sent to, for a UI to show as "we + texted ...". Present whenever `verificationRequired` is true. The + destination is the number already on file for the cardholder and cannot + be nominated by the caller. + example: '***0001' + alreadySent: + type: boolean + description: >- + Whether a code was already outstanding, so this call sent no new SMS. + Present whenever `verificationRequired` is true. It reads against the + call you made: on an opening, true means some earlier call left a code + outstanding; on a message, true means an earlier message already sent + one. Say "we already texted you" rather than implying a fresh send. + example: false + codeLength: + type: integer + description: >- + Number of digits in the code, read from the verification service's own + configuration. Absent when it could not be read — leave the input + unbounded in that case rather than guessing, since a guess that + disagrees with the service makes every correct code look like a typo. + example: 6 diff --git a/openapi/components/schemas/care/CareChatSession.yaml b/openapi/components/schemas/care/CareChatSession.yaml new file mode 100644 index 000000000..d251cd8d8 --- /dev/null +++ b/openapi/components/schemas/care/CareChatSession.yaml @@ -0,0 +1,44 @@ +type: object +description: >- + Proof that the cardholder answered their code, as a signed token the + partner sends back on every later message. +required: + - status + - sessionToken + - expiresAt +properties: + status: + type: string + enum: + - VERIFIED + description: Always `VERIFIED`; a code that did not match is an error, not a status. + example: VERIFIED + sessionToken: + type: string + description: >- + The value to send as the `X-Ls-Care-Session` header on every later + message on this chat. + + + **It is returned here and never again.** There is no route that reads + it back, and there is no revocation: the short expiry is what makes + that acceptable. Hold it in the cardholder's session on your side for + as long as they are chatting, and treat losing it as re-verification — + send a new code with `POST /care/chats/{chatId}/verifications` and + confirm it again. Never log it, never put it in a URL, and never store + it past `expiresAt`. + + + The token is bound to this platform, this chat and this cardholder, so + it cannot drive another chat and does not survive being handed to + another platform. + example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl + expiresAt: + type: string + format: date-time + description: >- + ISO 8601 timestamp, in UTC, after which the token is refused with + `CARE_SESSION_EXPIRED`. Sessions are short-lived by design — fifteen + minutes in the default deployment. Read this field rather than assuming + a duration. + example: '2026-09-12T14:25:00Z' diff --git a/openapi/components/schemas/care/CareChatVerificationConfirmRequest.yaml b/openapi/components/schemas/care/CareChatVerificationConfirmRequest.yaml new file mode 100644 index 000000000..59a996ee6 --- /dev/null +++ b/openapi/components/schemas/care/CareChatVerificationConfirmRequest.yaml @@ -0,0 +1,16 @@ +type: object +description: The code the cardholder read back from their SMS. +required: + - code +properties: + code: + type: string + minLength: 1 + maxLength: 32 + description: >- + The numeric code from the SMS, exactly as the cardholder entered it. + A code of the wrong shape is rejected with `INVALID_INPUT`; a + well-formed code that the provider says is not the one it sent is + rejected with `CARE_VERIFICATION_FAILED`, which is the error to retry + by asking the cardholder again. + example: '482913' diff --git a/openapi/components/schemas/care/CareChatVerificationRequest.yaml b/openapi/components/schemas/care/CareChatVerificationRequest.yaml new file mode 100644 index 000000000..dee0da4e7 --- /dev/null +++ b/openapi/components/schemas/care/CareChatVerificationRequest.yaml @@ -0,0 +1,21 @@ +type: object +description: >- + Options for re-sending the cardholder's verification code. The body may be + omitted entirely, which is the same as `{"resend": false}`. +properties: + resend: + type: boolean + default: false + description: >- + `false` asks for a code and is content to be told one is already + outstanding: the response then carries `alreadySent: true` and no new + SMS is sent. That is a success, not an error — the cardholder should + read back the code they already have. + + + `true` is the deliberate second code, for a cardholder who says the + first never arrived. Ask before you send one: codes are sent from a + pool of numbers, so a second code arrives from a different sender than + the first and leaves the cardholder holding two codes from two + strangers, deciding which to read back. + example: false diff --git a/openapi/components/schemas/care/CareChatVerificationResponse.yaml b/openapi/components/schemas/care/CareChatVerificationResponse.yaml new file mode 100644 index 000000000..819b38ec2 --- /dev/null +++ b/openapi/components/schemas/care/CareChatVerificationResponse.yaml @@ -0,0 +1,31 @@ +type: object +description: >- + Acknowledgement that a verification code is on its way to the cardholder. + `202`, not `200`: the SMS is in flight, not delivered. +required: + - status + - destinationMasked + - alreadySent +properties: + status: + type: string + enum: + - SENT + description: >- + Always `SENT`. The chat's own state is reported as + `verificationStatus` on the message route. + example: SENT + destinationMasked: + type: string + description: >- + The masked phone number the code is going to, for a UI to show as "we + texted ...". Always the number already on file for the cardholder; the + caller cannot nominate a destination. + example: '***0001' + alreadySent: + type: boolean + description: >- + True when a code was already outstanding and this call sent no new SMS. + Show "we already texted you" rather than implying a fresh send. A + request with `resend: true` sends a new code and returns false. + example: true diff --git a/openapi/components/schemas/care/CareChatVerificationStatus.yaml b/openapi/components/schemas/care/CareChatVerificationStatus.yaml new file mode 100644 index 000000000..177c6e536 --- /dev/null +++ b/openapi/components/schemas/care/CareChatVerificationStatus.yaml @@ -0,0 +1,26 @@ +type: string +enum: + - PENDING + - SENT + - VERIFIED + - TRUSTED + - FAILED +description: | + Where the cardholder's proof of presence stands on this chat, as of the + moment the call was received. A chat in `VERIFIED` or `TRUSTED` can reach + the cardholder's account, and every message on it must carry the + `X-Ls-Care-Session` header; a chat in any other status cannot, and the + agent answers general questions only. + + This field reports the chat as it was when the turn started. The agent can + verify a cardholder during a turn, so a chat that was verified by the turn + you just made still reads `SENT` in that response and `VERIFIED` on the next + call. + + | Status | Description | + |--------|-------------| + | `PENDING` | No code has been sent on this chat yet. This is where every new chat starts. | + | `SENT` | A code has been sent to the cardholder and is outstanding. | + | `VERIFIED` | The cardholder answered the code. Account-reaching tools are available and `POST /care/chats/{chatId}/verifications/confirm` has issued a session token. | + | `TRUSTED` | The cardholder is treated as present without an SMS code on this chat. Reserved for channels Grid can already attribute; a partner chat does not start here. | + | `FAILED` | The last code was answered incorrectly or expired. Send another code with `POST /care/chats/{chatId}/verifications`. | diff --git a/openapi/components/schemas/care/CareError.yaml b/openapi/components/schemas/care/CareError.yaml new file mode 100644 index 000000000..a5e222f3c --- /dev/null +++ b/openapi/components/schemas/care/CareError.yaml @@ -0,0 +1,72 @@ +type: object +description: | + The error body returned by every endpoint under **Embedded Care**. + + It carries `code` and `reason`, and nothing else. This differs from the + `Error4xx` / `Error5xx` schemas the rest of this specification uses, which + declare `status` and `message`: the care endpoints do not send those two + fields. Read the HTTP status from the response line and the human-readable + text from `reason`. + + Branch on `code`, never on `reason` — the text is written for a human + reading a log and may be reworded at any time. +required: + - code + - reason +properties: + code: + type: string + description: | + Machine-readable error code. Each endpoint's response descriptions say + which of these it can send at which status. + + | Error Code | HTTP | Description | + |------------|------|-------------| + | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `customerId` that is not a `Customer` id, a code of the wrong shape, or a chat that cannot be verified at all. | + | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused at the content boundary and no turn ran. `reason` carries the sentence to show them. | + | `CARE_VERIFICATION_FAILED` | 400 | The code did not match, or the chat's existing verification is too old to mint another session. Send a new code and confirm it. | + | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified — that is signalled in a `200` body. | + | `CARE_SESSION_EXPIRED` | 401 | The chat is verified, so it requires `X-Ls-Care-Session`, and the header was missing, malformed, or past its expiry. Send the cardholder a new code and confirm it. | + | `FORBIDDEN` | 403 | Embedded care is not enabled for this platform. | + | `CARE_SESSION_PLATFORM_MISMATCH` | 403 | The session token was minted for a different platform. Stop; retrying will not help. | + | `NOT_FOUND` | 404 | No such chat for this platform. A chat belonging to another platform, a chat that never existed, and a malformed chat id are one answer. | + | `CUSTOMER_NOT_FOUND` | 404 | No such cardholder for this platform. | + | `CONFLICT` | 409 | The `Idempotency-Key` was already used for a different message or a different chat — use a new key — or the chat has no cardholder bound to it. | + | `CARE_CHAT_ENDED` | 409 | The chat has been closed. Open a new one by sending an empty message to the cardholder. | + | `CARE_NOT_CONFIGURED_FOR_PLATFORM` | 422 | The platform is entitled to embedded care but has no care programme configured. | + | `CARE_BUDGET_EXCEEDED` | 429 | This chat has reached its limit. It is not replaced by a new one: a replacement would be the limit cleared by asking again. | + | `RATE_LIMITED` | 429 | The platform is calling this endpoint too often. Back off for the interval in the `Retry-After` response header. | + | `INTERNAL_ERROR` | 500 | Unexpected server error. | + | `CARE_TURN_FAILED` | 502 | The agent could not answer. Transient; the same `Idempotency-Key` may be sent again. | + | `CARE_VERIFICATION_UNAVAILABLE` | 502 | The SMS provider could not be reached. Distinct from a wrong code: the factor is down, so retry rather than asking the cardholder again. | + | `SERVICE_UNAVAILABLE` | 503 | Embedded care is not available in this deployment, or the support agent is temporarily turned off. | + | `CARE_TURN_TIMEOUT` | 504 | The turn is still running. Retry with the same `Idempotency-Key` to collect the answer. | + enum: + - INVALID_INPUT + - CARE_MESSAGE_REJECTED + - CARE_VERIFICATION_FAILED + - UNAUTHORIZED + - CARE_SESSION_EXPIRED + - FORBIDDEN + - CARE_SESSION_PLATFORM_MISMATCH + - NOT_FOUND + - CUSTOMER_NOT_FOUND + - CONFLICT + - CARE_CHAT_ENDED + - CARE_NOT_CONFIGURED_FOR_PLATFORM + - CARE_BUDGET_EXCEEDED + - RATE_LIMITED + - INTERNAL_ERROR + - CARE_TURN_FAILED + - CARE_VERIFICATION_UNAVAILABLE + - SERVICE_UNAVAILABLE + - CARE_TURN_TIMEOUT + example: CARE_SESSION_EXPIRED + reason: + type: string + description: >- + Human-readable explanation, for a log or a support ticket. Not for + branching, and not always safe to show a cardholder verbatim — the + exception is `CARE_MESSAGE_REJECTED`, whose `reason` is written to be + shown. + example: The care session has expired. Verify the cardholder again. diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 35a564c23..880c401b2 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -97,6 +97,73 @@ tags: Stablecoin issuance endpoints. Link provider accounts, register provider-created stablecoins, create direct mint/burn issuer operations, and track operation status. + - name: Embedded Care + description: | + A support chat between the cardholder and Lightspark's care agent, driven + from the partner's own app. The platform calls with its ordinary Grid + credential, but the person typing is the cardholder rather than the + caller, so reaching that cardholder's account takes a second proof: an + SMS code they answer, which the partner afterwards carries in the + `X-Ls-Care-Session` header. + + **The surface is four routes, and the happy path is two calls.** + + 1. **Open the chat.** `POST /care/chats/{chatId}/messages` with an empty + `message`, addressed to the cardholder as `Customer:`. Returns + the chat's id and the agent's `greeting`, which is what a chat window + renders before anybody has typed. Sends no SMS, runs no turn, bills + nothing, and is safe to call again — a widget that reloaded gets the + same chat back, this time without a greeting. + 2. **Send the cardholder's first message.** The same route with their + text and an `Idempotency-Key` header. One turn runs synchronously and + the agent's answer comes back in `reply`. On a chat that cannot yet + reach the account this call also texts the verification code and says + so in the same body: `verificationRequired: true`, with + `destinationMasked`, `alreadySent` and `codeLength` for the input you + put on screen. + 3. **Confirm the code.** + `POST /care/chats/{chatId}/verifications/confirm` returns a + `sessionToken` and its `expiresAt`. Send that token as the + `X-Ls-Care-Session` header on every message from then on. + 4. **Close the chat** with `DELETE /care/chats/{chatId}` when the + cardholder is done. + + `POST /care/chats/{chatId}/verifications` is the *Send another code* + button rather than a step in the flow: step 2 already sent the first + code. + + Step 1 is optional. A message addressed to a `Customer:` id opens that + cardholder's chat if they have none, so a partner whose cardholder has + just typed something spends one call rather than two. + + A cardholder who has not verified yet is never a `401`. That state is + reported inside a `200`, because the first message from every new + cardholder is unverified and a `401` there would be indistinguishable + from a bad credential. On these endpoints a `401` means the platform's + Grid credential is wrong, or a verified chat was driven without its + session token. + + Two things an integration has to get right: + + - **Render `displayMessage`, never your own copy of what was typed.** + Grid redacts the cardholder's text on the way in, and `displayMessage` + is that redacted echo. A UI that renders its own input box instead has + put a card number the cardholder pasted back on their screen unmasked, + and into every log the transcript reaches. + - **The session token is returned once and is never re-readable.** No + endpoint hands it back, there is no revocation, and it is short-lived + by design. Hold it for the life of the cardholder's session; a partner + that drops it has to send a new code and confirm it again. + + Some things are absent by construction, so no integration can come to + depend on them: nothing returns a transcript or an earlier turn's reply + (retrying under the original `Idempotency-Key` is how an answer that was + produced but not delivered is recovered), nothing sets a verification + state, and nothing changes the number a code is sent to. + + Errors from these endpoints carry `code` and `reason` only — see the + `CareError` schema, which differs from the `Error4xx` schemas used + elsewhere in this specification. servers: - url: https://api.lightspark.com/grid/2025-10-13 description: Production server @@ -390,6 +457,14 @@ paths: $ref: paths/stablecoins/stablecoin-provider-accounts.yaml /stablecoin-provider-accounts/{stablecoinProviderAccountId}: $ref: paths/stablecoins/stablecoin-provider-accounts_{stablecoinProviderAccountId}.yaml + /care/chats/{chatId}/messages: + $ref: paths/care/care_chats_{chatId}_messages.yaml + /care/chats/{chatId}/verifications: + $ref: paths/care/care_chats_{chatId}_verifications.yaml + /care/chats/{chatId}/verifications/confirm: + $ref: paths/care/care_chats_{chatId}_verifications_confirm.yaml + /care/chats/{chatId}: + $ref: paths/care/care_chats_{chatId}.yaml webhooks: agent-action: $ref: webhooks/agent-action.yaml diff --git a/openapi/paths/care/care_chats_{chatId}.yaml b/openapi/paths/care/care_chats_{chatId}.yaml new file mode 100644 index 000000000..01bd81df8 --- /dev/null +++ b/openapi/paths/care/care_chats_{chatId}.yaml @@ -0,0 +1,78 @@ +parameters: + - name: chatId + in: path + description: >- + The chat to close, as returned in `chatId` by + `POST /care/chats/{chatId}/messages`. + required: true + schema: + type: string + example: CareChat:019542f5-b3e7-1d02-0000-000000000030 +delete: + summary: End a care chat + description: | + Close a chat the cardholder has finished with or navigated away from. The + conversation stops accepting messages; sending one afterwards is refused + with `CARE_CHAT_ENDED`, and the cardholder starts a new chat by sending + another message. + + No session token is required, even on a verified chat. Ending a chat is + the one thing the second factor is exempt from: it reaches nothing but the + chat's own closing timestamp, and requiring a live session would mean a + partner whose cardholder walked away could not close the chat without + texting them another code first. Ownership of the chat by the calling + platform is the whole bound. + + Closing a chat that is already closed is not an error — that is the state + the caller asked for. + operationId: endCareChat + tags: + - Embedded Care + security: + - BasicAuth: [] + responses: + '204': + description: The chat is closed. No content. + '401': + description: '`UNAUTHORIZED` — the platform''s Grid credential is missing or wrong.' + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '403': + description: '`FORBIDDEN` — embedded care is not enabled for this platform.' + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '404': + description: >- + `NOT_FOUND` — no such chat for this platform. A chat that belongs to + another platform, one that never existed, and a malformed chat id are + one answer. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '429': + description: >- + `RATE_LIMITED` — the platform is calling this endpoint too often. Back + off for the interval in the `Retry-After` header. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '500': + description: Internal service error. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '503': + description: >- + `SERVICE_UNAVAILABLE` when embedded care is not available in this + deployment, or the support agent is temporarily turned off. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml diff --git a/openapi/paths/care/care_chats_{chatId}_messages.yaml b/openapi/paths/care/care_chats_{chatId}_messages.yaml new file mode 100644 index 000000000..971d6e0fa --- /dev/null +++ b/openapi/paths/care/care_chats_{chatId}_messages.yaml @@ -0,0 +1,238 @@ +parameters: + - name: chatId + in: path + description: >- + Who this message is for. Two forms are accepted, and the id's own prefix + decides which: a `Customer:` addresses the cardholder and lands on + their open chat, opening one if they have none; a `CareChat:` + addresses that chat and no other. A bare uuid is read as a chat id. + Addressing the cardholder is what lets a partner send their first + message in one call instead of two. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 +post: + summary: Send a care chat message + description: | + 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. + + **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 reads a transcript, so a + reply that was produced but not delivered is reachable only by retrying + under the same key, which returns the recorded answer with + `replayed: true`. 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 nothing.** On a + cardholder with no open chat it opens one and answers with `greeting`, 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 `greeting` does not, because + a greeting arriving mid-conversation reads as the agent introducing itself + a second time. Either way no code is sent, no `Idempotency-Key` is needed + or consumed, and nothing is billed — a widget in a reconnect loop may call + it as often as it likes. + + **Verification is signalled in this `200` body, never as a `401`.** Every + new cardholder's first message is unverified, so a `401` there would make + the normal case indistinguishable from a bad credential. A chat that + cannot yet reach the account answers `200` with + `verificationRequired: true`, and the first message with text is what + sends the code — `destinationMasked`, `alreadySent` and, where readable, + `codeLength` come back with it, and the agent's own sentence in `reply` + asks the cardholder for the code. Later messages on the same chat find the + code already outstanding and send nothing, reporting `alreadySent: true`. + Collect the code and post it to + `POST /care/chats/{chatId}/verifications/confirm`. + + **Render `displayMessage`, not your own copy of the input.** Grid redacts + the cardholder's text on the way in — a card number they pasted comes back + masked — and `displayMessage` is that redacted echo. A UI that echoes its + own input box instead has put an unmasked PAN in the transcript, on the + screen and in whatever logs the transcript reaches. + + Once the chat is verified every message must carry the session token from + `POST /care/chats/{chatId}/verifications/confirm` in the + `X-Ls-Care-Session` header. Without it the call is refused with + `CARE_SESSION_EXPIRED`: the chat's verified state never expires on its + own, so the session is what proves the cardholder is still at the keyboard. + + Everything in the response describes the chat as of the moment the turn + started, `verificationStatus` included. The agent can verify a cardholder + during a turn, so a chat that this very call verified still reads + unverified here and verified on the next call. + operationId: sendCareChatMessage + tags: + - Embedded Care + security: + - BasicAuth: [] + parameters: + - name: Idempotency-Key + in: header + required: false + description: >- + A unique identifier for this turn, up to 255 characters. **Required + whenever `message` is non-empty**; omitting it there is rejected with + `INVALID_INPUT`. A retry carrying the same key returns the first + call's answer with `replayed: true`; aiming the same key at a + different message, a different chat or a different cardholder is + rejected with `409 CONFLICT`. An opening (empty `message`) neither + needs nor consumes a key — one sent anyway is ignored, so that the + partner's first real message can still use it. + schema: + type: string + maxLength: 255 + example: 550e8400-e29b-41d4-a716-446655440000 + - name: X-Ls-Care-Session + in: header + required: false + description: >- + The `sessionToken` from + `POST /care/chats/{chatId}/verifications/confirm`. Required on every + message once the chat is verified, and refused with + `CARE_SESSION_EXPIRED` when it is missing, malformed or past its + expiry. Not read before verification, and not read on an opening — a + widget that reloaded has lost its token, which is the case the opening + exists for. + schema: + type: string + example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl + requestBody: + required: false + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareChatMessageRequest.yaml + examples: + openTheChat: + summary: Open or resume the cardholder's chat, saying nothing + value: + message: '' + firstMessage: + summary: The cardholder's first message, which also sends the code + value: + message: My card was declined at the grocery store + afterVerification: + summary: A later message, sent with the X-Ls-Care-Session header + value: + message: Can you tell me the last four digits on file? + responses: + '200': + description: >- + The turn ran, or the chat was opened or resumed. Which fields are + present depends on which of those happened. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareChatMessageResponse.yaml + '400': + description: >- + 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 at + the content boundary — no turn ran, and `reason` is written to be + shown to them. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '401': + description: >- + `UNAUTHORIZED` when the platform's Grid credential is missing or + wrong. `CARE_SESSION_EXPIRED` when the chat is verified and the + `X-Ls-Care-Session` header is missing, malformed or expired. An + unverified cardholder is never a `401` — that is a `200` carrying + `verificationRequired`. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '403': + description: >- + `FORBIDDEN` when embedded care is not enabled for this platform, or + `CARE_SESSION_PLATFORM_MISMATCH` when the session token was minted for + another platform. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '404': + description: >- + `NOT_FOUND` when the path names a chat this platform does not own, and + `CUSTOMER_NOT_FOUND` when it names a cardholder this platform does not + own. A resource that belongs to somebody else and one that never + existed are the same answer. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '409': + description: >- + `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 the chat has been + closed; open a new one by sending an empty message to the cardholder. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '422': + description: >- + `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. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '429': + description: >- + `CARE_BUDGET_EXCEEDED` when this chat has reached its limit. The chat + is refused rather than replaced: opening a fresh one would be the + limit cleared by asking again. `RATE_LIMITED` when the platform is + calling this endpoint too often; back off for the interval in the + `Retry-After` header. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '500': + description: Internal service error. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '502': + description: >- + `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`. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '503': + description: >- + `SERVICE_UNAVAILABLE` when embedded care is not available in this + deployment, or the support agent is temporarily turned off. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '504': + description: >- + `CARE_TURN_TIMEOUT` when the turn is still running. The answer is not + lost — retry with the same `Idempotency-Key` to collect it. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml diff --git a/openapi/paths/care/care_chats_{chatId}_verifications.yaml b/openapi/paths/care/care_chats_{chatId}_verifications.yaml new file mode 100644 index 000000000..054d8967d --- /dev/null +++ b/openapi/paths/care/care_chats_{chatId}_verifications.yaml @@ -0,0 +1,138 @@ +parameters: + - name: chatId + in: path + description: >- + The chat to send another code on, as returned in `chatId` by + `POST /care/chats/{chatId}/messages`. + required: true + schema: + type: string + example: CareChat:019542f5-b3e7-1d02-0000-000000000030 +post: + summary: Resend a care chat verification code + description: | + Send the cardholder another verification code. + + **This is the resend, not the first send.** The cardholder's first message + already sent a code and said so in its own body; this is what the *Send + another code* button calls when they say it never arrived. A partner never + has to call it to start verification. + + With `resend` false — the default, and the same as sending no body — the + call asks for a code and is content to be told one is already outstanding: + the answer carries `alreadySent: true` and no new SMS goes out. That is a + success. With `resend` true a second code is sent. Ask the cardholder + before doing that: codes are sent from a pool of numbers, so the second + arrives from a different sender than the first, leaving them holding two + codes from two strangers and deciding which to read back. + + The destination is always the number already on file for the cardholder. + There is no way to nominate one, because a code sent to a number the + caller chose would prove nothing about the person holding the card. + + `202`, not `200`: the SMS is in flight, not delivered. + + A chat that is already verified is refused with `INVALID_INPUT` while its + verification is recent — there is nothing left to prove. Once that + verification has aged past the session lifetime the refusal lifts and a + fresh code can be sent. + operationId: resendCareChatVerification + tags: + - Embedded Care + security: + - BasicAuth: [] + requestBody: + required: false + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareChatVerificationRequest.yaml + examples: + askForACode: + summary: Ask for a code, accepting the one already outstanding + value: + resend: false + deliberateResend: + summary: The cardholder says the first code never arrived + value: + resend: true + responses: + '202': + description: >- + The code is on its way, or one was already outstanding and none was + sent — `alreadySent` says which. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareChatVerificationResponse.yaml + '400': + description: >- + `INVALID_INPUT` when the chat is already verified, when the body + carries an unknown field, or when verification is not available for + this chat at all — for example a cardholder with no phone number on + file. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '401': + description: '`UNAUTHORIZED` — the platform''s Grid credential is missing or wrong.' + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '403': + description: '`FORBIDDEN` — embedded care is not enabled for this platform.' + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '404': + description: >- + `NOT_FOUND` — no such chat for this platform. A chat that belongs to + another platform, one that never existed, and a malformed chat id are + one answer. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '409': + description: >- + `CARE_CHAT_ENDED` when the chat has been closed, or `CONFLICT` when it + has no cardholder bound to it. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '429': + description: >- + `CARE_BUDGET_EXCEEDED` when this chat has reached its limit, or + `RATE_LIMITED` when the platform is calling this endpoint too often; + back off for the interval in the `Retry-After` header. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '500': + description: Internal service error. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '502': + description: >- + `CARE_VERIFICATION_UNAVAILABLE` — the SMS provider could not be + reached. The factor is down, so retry rather than asking the + cardholder for anything. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '503': + description: >- + `SERVICE_UNAVAILABLE` when embedded care is not available in this + deployment, or the support agent is temporarily turned off. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml diff --git a/openapi/paths/care/care_chats_{chatId}_verifications_confirm.yaml b/openapi/paths/care/care_chats_{chatId}_verifications_confirm.yaml new file mode 100644 index 000000000..2bd16e1c8 --- /dev/null +++ b/openapi/paths/care/care_chats_{chatId}_verifications_confirm.yaml @@ -0,0 +1,133 @@ +parameters: + - name: chatId + in: path + description: >- + The chat the code was sent on, as returned in `chatId` by + `POST /care/chats/{chatId}/messages`. + required: true + schema: + type: string + example: CareChat:019542f5-b3e7-1d02-0000-000000000030 +post: + summary: Confirm a care chat verification code + description: | + Check the code the cardholder read back, and hand over the session that + every later message on this chat rides on. + + **The session token is returned here and never again.** No endpoint reads + it back, and there is no revocation — the short expiry is what makes that + acceptable. Keep it in the cardholder's own session on your side, send it + as the `X-Ls-Care-Session` header on every later message, and treat losing + it as re-verification: send a new code with + `POST /care/chats/{chatId}/verifications` and confirm it again. Read + `expiresAt` rather than assuming a lifetime. + + The token is bound to this platform, this chat and this cardholder. It + cannot drive another chat, and presenting it to another platform's chat is + refused with `CARE_SESSION_PLATFORM_MISMATCH`. + + A code that does not match is `CARE_VERIFICATION_FAILED` — ask the + cardholder again. A provider that could not be reached is + `CARE_VERIFICATION_UNAVAILABLE` — retry; nothing the cardholder does will + help. The two are kept apart deliberately, because they call for opposite + reactions. + + A chat whose verification is older than the session lifetime will not mint + a fresh session off it: that would be the platform issuing itself a + session for a cardholder who has not shown themselves in longer than a + session lasts. It is refused with `CARE_VERIFICATION_FAILED`, and the way + forward is a new code. + operationId: confirmCareChatVerification + tags: + - Embedded Care + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareChatVerificationConfirmRequest.yaml + example: + code: '482913' + responses: + '200': + description: >- + The cardholder is verified on this chat, and the session token is in + this body — it is not retrievable afterwards. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareChatSession.yaml + '400': + description: >- + `CARE_VERIFICATION_FAILED` when the code did not match, or when the + chat's existing verification is too old to mint another session. + `INVALID_INPUT` when the code is not of the shape the SMS carries, + when the body is missing or carries an unknown field, or when + verification is not available for this chat. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '401': + description: '`UNAUTHORIZED` — the platform''s Grid credential is missing or wrong.' + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '403': + description: '`FORBIDDEN` — embedded care is not enabled for this platform.' + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '404': + description: >- + `NOT_FOUND` — no such chat for this platform. A chat that belongs to + another platform, one that never existed, and a malformed chat id are + one answer. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '409': + description: >- + `CARE_CHAT_ENDED` when the chat has been closed, or `CONFLICT` when it + has no cardholder bound to it. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '429': + description: >- + `CARE_BUDGET_EXCEEDED` when this chat has reached its limit, or + `RATE_LIMITED` when the platform is calling this endpoint too often; + back off for the interval in the `Retry-After` header. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '500': + description: Internal service error. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '502': + description: >- + `CARE_VERIFICATION_UNAVAILABLE` — the SMS provider could not be + reached, so the code could not be checked. Retry; the cardholder has + done nothing wrong. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml + '503': + description: >- + `SERVICE_UNAVAILABLE` when embedded care is not available in this + deployment, or the support agent is temporarily turned off. + content: + application/json: + schema: + $ref: ../../components/schemas/care/CareError.yaml From 43dadba622ee799371c56dd3b998168e2b1aa57c Mon Sep 17 00:00:00 2001 From: kphurley7 Date: Sat, 12 Sep 2026 19:27:52 -0500 Subject: [PATCH 02/11] docs(care): say what makes a resend unavailable, rather than guessing The 400 on the resend route named "a cardholder with no phone number on file" as the example. The two branches that produce it are a care programme whose policy does not offer SMS verification and a cardholder record that could not be read; a missing phone number takes the send path and fails as a provider error instead. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TDh3ab6V4rrpVG4v8cjj1u --- mintlify/openapi.yaml | 2 +- openapi.yaml | 2 +- openapi/paths/care/care_chats_{chatId}_verifications.yaml | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 97c8c3437..2c9e0d8a4 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -10872,7 +10872,7 @@ paths: schema: $ref: '#/components/schemas/CareChatVerificationResponse' '400': - description: '`INVALID_INPUT` when the chat is already verified, when the body carries an unknown field, or when verification is not available for this chat at all — for example a cardholder with no phone number on file.' + description: '`INVALID_INPUT` when the chat is already verified, when the body carries an unknown field, or when verification is not available for this chat at all — the platform''s care programme does not offer SMS verification, or the cardholder record behind the chat could not be read.' content: application/json: schema: diff --git a/openapi.yaml b/openapi.yaml index 97c8c3437..2c9e0d8a4 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -10872,7 +10872,7 @@ paths: schema: $ref: '#/components/schemas/CareChatVerificationResponse' '400': - description: '`INVALID_INPUT` when the chat is already verified, when the body carries an unknown field, or when verification is not available for this chat at all — for example a cardholder with no phone number on file.' + description: '`INVALID_INPUT` when the chat is already verified, when the body carries an unknown field, or when verification is not available for this chat at all — the platform''s care programme does not offer SMS verification, or the cardholder record behind the chat could not be read.' content: application/json: schema: diff --git a/openapi/paths/care/care_chats_{chatId}_verifications.yaml b/openapi/paths/care/care_chats_{chatId}_verifications.yaml index 054d8967d..b6f4af439 100644 --- a/openapi/paths/care/care_chats_{chatId}_verifications.yaml +++ b/openapi/paths/care/care_chats_{chatId}_verifications.yaml @@ -69,8 +69,9 @@ post: description: >- `INVALID_INPUT` when the chat is already verified, when the body carries an unknown field, or when verification is not available for - this chat at all — for example a cardholder with no phone number on - file. + this chat at all — the platform's care programme does not offer SMS + verification, or the cardholder record behind the chat could not be + read. content: application/json: schema: From 3d33538c11b3357193a5cd9cb6e957a0e3ef26fb Mon Sep 17 00:00:00 2001 From: kphurley7 Date: Wed, 16 Sep 2026 11:26:24 -0500 Subject: [PATCH 03/11] docs(care): bring the chat endpoints up to date with what care implements Three changes landed in sparkcore after this spec was written, and the spec described the surface as it was before them. In-chat verification now mints the session. A cardholder can read their code back to the agent as an ordinary message, and that turn's response carries sessionToken and expiresAt, the same two fields the confirm route returns. The message response schema now declares them, and the message route, the confirm route and the tag overview say that the credential arrives from whichever door took the code. Account access is now freshness-aware. A chat reaches the cardholder's account only while its verification is newer than the session lifetime, so the old claim that "the chat's verified state never expires on its own" is gone. Past that window the session header stops being required and the next message sends a fresh code and has the agent ask for it, rather than failing. A dead token sent anyway is still a 401, so the recovery is stated: drop the header and resend. verificationStatus is no longer a proxy for what the agent may do. A stale chat reports VERIFIED beside verificationRequired true, and the schemas now tell a client to branch on verificationRequired. Also corrected: a session token minted for another chat or another cardholder answers 404, not a session error; CARE_CHAT_ENDED is raised by a message carrying text rather than by an opening. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TDh3ab6V4rrpVG4v8cjj1u --- mintlify/openapi.yaml | 174 ++++++++++++++---- openapi.yaml | 174 ++++++++++++++---- .../schemas/care/CareChatMessageResponse.yaml | 61 +++++- .../schemas/care/CareChatSession.yaml | 16 +- .../care/CareChatVerificationStatus.yaml | 20 +- .../components/schemas/care/CareError.yaml | 6 +- openapi/openapi.yaml | 26 ++- .../care/care_chats_{chatId}_messages.yaml | 99 +++++++--- .../care_chats_{chatId}_verifications.yaml | 10 +- ..._chats_{chatId}_verifications_confirm.yaml | 15 +- 10 files changed, 493 insertions(+), 108 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 2c9e0d8a4..18a918e34 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -88,7 +88,10 @@ tags: 3. **Confirm the code.** `POST /care/chats/{chatId}/verifications/confirm` returns a `sessionToken` and its `expiresAt`. Send that token as the - `X-Ls-Care-Session` header on every message from then on. + `X-Ls-Care-Session` header on every message from then on. A + cardholder who reads the code back to the agent as an ordinary + message instead is confirmed the same way, and that turn's own + response carries the same two fields. 4. **Close the chat** with `DELETE /care/chats/{chatId}` when the cardholder is done. @@ -104,8 +107,21 @@ tags: reported inside a `200`, because the first message from every new cardholder is unverified and a `401` there would be indistinguishable from a bad credential. On these endpoints a `401` means the platform's - Grid credential is wrong, or a verified chat was driven without its - session token. + Grid credential is wrong, or a chat whose verification is still current + was driven without its session token. + + **Verification expires, and expiry is recoverable in the chat.** 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 also how long a session token lives. Past that window the chat + stops requiring the header: a message sent without one gets a fresh + code, and the agent asks the cardholder to read it back inside the + conversation, which hands the partner the next session token. Sending + the dead token anyway is still a `401 CARE_SESSION_EXPIRED`, so the + handling is to discard the token on that error and resend the message + with no `X-Ls-Care-Session` header rather than to tear the chat down. + If that resend is refused too, the chat's verification is still current + and the way back is a new code. Two things an integration has to get right: @@ -117,7 +133,9 @@ tags: - **The session token is returned once and is never re-readable.** No endpoint hands it back, there is no revocation, and it is short-lived by design. Hold it for the life of the cardholder's session; a partner - that drops it has to send a new code and confirm it again. + that drops it has to send a new code and confirm it again. It arrives + from whichever door took the code, so read `sessionToken` on the + message response as well as on the confirm response. Some things are absent by construction, so no integration can come to depend on them: nothing returns a transcript or an earlier turn's reply @@ -10684,16 +10702,59 @@ paths: own input box instead has put an unmasked PAN in the transcript, on the screen and in whatever logs the transcript reaches. - Once the chat is verified every message must carry the session token from - `POST /care/chats/{chatId}/verifications/confirm` in the - `X-Ls-Care-Session` header. Without it the call is refused with - `CARE_SESSION_EXPIRED`: the chat's verified state never expires on its - own, so the session is what proves the cardholder is still at the keyboard. + **The code can also be read back to the agent, and then this route issues + the session.** When the cardholder types their code as a message rather + than into your code input, the agent puts it to the same provider, through + the same check `POST /care/chats/{chatId}/verifications/confirm` performs, + and the chat comes out of that turn able to reach the account. That turn's + body carries `sessionToken` and `expiresAt`, under the same names and with + the same meaning as the confirm route's, so one handler in your app serves + both doors. 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, or you will hold a chat that is privileged and unusable at + once. + + The structured route is not deprecated by this, and it is still the one to + call when your own UI collects the code: only it can tell you where the + code went, whether one was already in flight, and how many digits it has. + + While the chat's verification is current, every message must carry the + session token in the `X-Ls-Care-Session` header, whichever of the two + doors issued it. Without it the call is refused with + `CARE_SESSION_EXPIRED`. + + **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 chat no longer reaches the account, the header stops + being required, and the next message **sent without it** is treated as an + unverified one: a fresh code goes out, the body carries + `verificationRequired: true` with `destinationMasked`, `alreadySent` and + `codeLength`, and the agent's own `reply` asks the cardholder to read the + code back. The turn still runs and still answers. + + Sending the expired token anyway is still a `401 CARE_SESSION_EXPIRED`. + One recovery covers a token that was lost, one that was never valid and + one that has expired: retry the message with no `X-Ls-Care-Session` + header. On a chat whose verification has gone stale that call succeeds, + sends a fresh code and has the agent ask for it, and the turn in which the + cardholder reads it back returns the next `sessionToken`. On a chat whose + verification is still current it is refused with `CARE_SESSION_EXPIRED` + again, which is the signal to send a code with + `POST /care/chats/{chatId}/verifications` and confirm it. 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 describes the chat as of the moment the turn started, `verificationStatus` included. The agent can verify a cardholder during a turn, so a chat that this very call verified still reads - unverified here and verified on the next call. + unverified here and verified on the next call. For the same reason a chat + whose verification has gone stale reports `verificationStatus: VERIFIED` + beside `verificationRequired: true`. Branch on `verificationRequired`, + never on `verificationStatus`. operationId: sendCareChatMessage tags: - Embedded Care @@ -10711,7 +10772,7 @@ paths: - name: X-Ls-Care-Session in: header required: false - description: The `sessionToken` from `POST /care/chats/{chatId}/verifications/confirm`. Required on every message once the chat is verified, and refused with `CARE_SESSION_EXPIRED` when it is missing, malformed or past its expiry. Not read before verification, and not read on an opening — a widget that reloaded has lost its token, which is the case the opening exists for. + description: The `sessionToken` from `POST /care/chats/{chatId}/verifications/confirm`, or the one this route returned on the turn that verified the cardholder in the conversation. Required on every message while the chat's verification is current, and refused with `CARE_SESSION_EXPIRED` when it is missing there. Not required before the cardholder has verified, nor once the chat's verification has aged past the session lifetime; but a token that is sent is always checked, so a malformed or expired one is refused with `CARE_SESSION_EXPIRED` whatever state the chat is in, and one minted for another chat or another cardholder is `NOT_FOUND`. An opening never reads the header at all, because a widget that reloaded has lost its token and that is the case the opening exists for. schema: type: string example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl @@ -10748,7 +10809,7 @@ paths: schema: $ref: '#/components/schemas/CareError' '401': - description: '`UNAUTHORIZED` when the platform''s Grid credential is missing or wrong. `CARE_SESSION_EXPIRED` when the chat is verified and the `X-Ls-Care-Session` header is missing, malformed or expired. An unverified cardholder is never a `401` — that is a `200` carrying `verificationRequired`.' + description: '`UNAUTHORIZED` when the platform''s Grid credential is missing or wrong. `CARE_SESSION_EXPIRED` when the chat''s verification is still current and the `X-Ls-Care-Session` header is missing, or when the token presented is malformed or past its expiry. An unverified cardholder is never a `401`; that is a `200` carrying `verificationRequired`. Neither is a chat whose verification has gone stale, which is asked for a code again inside a `200`.' content: application/json: schema: @@ -10760,13 +10821,13 @@ paths: schema: $ref: '#/components/schemas/CareError' '404': - description: '`NOT_FOUND` when the path names a chat this platform does not own, and `CUSTOMER_NOT_FOUND` when it names a cardholder this platform does not own. A resource that belongs to somebody else and one that never existed are the same answer.' + description: '`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.' content: application/json: schema: $ref: '#/components/schemas/CareError' '409': - description: '`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 the chat has been closed; open a new one by sending an empty message to the cardholder.' + description: '`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 carrying text is sent to a chat that has been closed; open a new one by sending a message addressed to the cardholder as `Customer:`, which finds no open chat and opens a fresh one.' content: application/json: schema: @@ -10824,7 +10885,9 @@ paths: **This is the resend, not the first send.** The cardholder's first message already sent a code and said so in its own body; this is what the *Send another code* button calls when they say it never arrived. A partner never - has to call it to start verification. + has to call it to start verification, and does not have to call it to + recover a chat whose verification has gone stale either, because the next + message sends a code by itself. With `resend` false — the default, and the same as sending no body — the call asks for a code and is content to be told one is already outstanding: @@ -10842,8 +10905,10 @@ paths: A chat that is already verified is refused with `INVALID_INPUT` while its verification is recent — there is nothing left to prove. Once that - verification has aged past the session lifetime the refusal lifts and a - fresh code can be sent. + verification has aged past the session lifetime the chat no longer reaches + the cardholder's account, so there is something to prove again: the + refusal lifts on its own and this route sends a fresh code, whatever + `verificationStatus` still says. operationId: resendCareChatVerification tags: - Embedded Care @@ -10948,6 +11013,13 @@ paths: `POST /care/chats/{chatId}/verifications` and confirm it again. Read `expiresAt` rather than assuming a lifetime. + **This is not the only door.** A cardholder can read the code back to the + agent as an ordinary message, and `POST /care/chats/{chatId}/messages` + then returns the same `sessionToken` and `expiresAt` on that turn. Call + this route when your own UI collects the code, because only it can tell + you where the code went, whether one was already in flight, and how many + digits it has. + The token is bound to this platform, this chat and this cardholder. It cannot drive another chat, and presenting it to another platform's chat is refused with `CARE_SESSION_PLATFORM_MISMATCH`. @@ -10961,8 +11033,12 @@ paths: A chat whose verification is older than the session lifetime will not mint a fresh session off it: that would be the platform issuing itself a session for a cardholder who has not shown themselves in longer than a - session lasts. It is refused with `CARE_VERIFICATION_FAILED`, and the way - forward is a new code. + session lasts. It is refused with `CARE_VERIFICATION_FAILED` before the + provider is reached, and the way forward is a new code. That is the same + window that decides whether the chat reaches the cardholder's account at + all, so a stale chat is not a locked one: sending a code with + `POST /care/chats/{chatId}/verifications`, or simply sending the next + message and letting the agent ask, both open it again. operationId: confirmCareChatVerification tags: - Embedded Care @@ -27883,10 +27959,18 @@ components: - FAILED description: | Where the cardholder's proof of presence stands on this chat, as of the - moment the call was received. A chat in `VERIFIED` or `TRUSTED` can reach - the cardholder's account, and every message on it must carry the - `X-Ls-Care-Session` header; a chat in any other status cannot, and the - agent answers general questions only. + moment the call was received. + + **This status alone does not say whether the agent can reach the account.** + A chat reaches it only while it is in `VERIFIED` or `TRUSTED` *and* the + verification behind that status is newer than the session lifetime, fifteen + minutes in the default deployment. Once it ages past that window the status + still reads `VERIFIED`, the chat no longer reaches the account, the + `X-Ls-Care-Session` header stops being required, and the next message sends + a fresh code and has the agent ask for it. The field that answers "can the + agent act now" is `verificationRequired` on the message response, so branch + on that and treat this status as a record of what has happened rather than + of what is permitted. This field reports the chat as it was when the turn started. The agent can verify a cardholder during a turn, so a chat that was verified by the turn @@ -27897,8 +27981,8 @@ components: |--------|-------------| | `PENDING` | No code has been sent on this chat yet. This is where every new chat starts. | | `SENT` | A code has been sent to the cardholder and is outstanding. | - | `VERIFIED` | The cardholder answered the code. Account-reaching tools are available and `POST /care/chats/{chatId}/verifications/confirm` has issued a session token. | - | `TRUSTED` | The cardholder is treated as present without an SMS code on this chat. Reserved for channels Grid can already attribute; a partner chat does not start here. | + | `VERIFIED` | The cardholder answered the code, either at `POST /care/chats/{chatId}/verifications/confirm` or by reading it back to the agent. Account-reaching tools are available for as long as that answer is newer than the session lifetime, and a session token was issued by whichever of the two doors took the code. | + | `TRUSTED` | The cardholder is treated as present without an SMS code on this chat. Reserved for channels Grid can already attribute; a partner chat does not start here. Bounded by the same freshness window as `VERIFIED`. | | `FAILED` | The last code was answered incorrectly or expired. Send another code with `POST /care/chats/{chatId}/verifications`. | CareChatGreeting: type: object @@ -27923,7 +28007,7 @@ components: example: '2026-09-12T14:10:00.123456Z' CareChatMessageResponse: type: object - description: 'The result of one call to the message route. Which fields are present depends on what the call did: a turn adds `id`, `displayMessage`, `reply` and `replayed`; the call that opened the chat adds `greeting`; an outstanding verification adds `destinationMasked`, `alreadySent` and, where the service could be read, `codeLength`.' + description: 'The result of one call to the message route. Which fields are present depends on what the call did: a turn adds `id`, `displayMessage`, `reply` and `replayed`; the call that opened the chat adds `greeting`; an outstanding verification adds `destinationMasked`, `alreadySent` and, where the service could be read, `codeLength`; and a turn in which the cardholder read their code back to the agent adds `sessionToken` and `expiresAt`.' required: - chatId - verificationStatus @@ -27937,7 +28021,12 @@ components: $ref: '#/components/schemas/CareChatVerificationStatus' verificationRequired: type: boolean - description: 'Whether the cardholder still has to prove they are present before the agent can reach their account. Present on every response, true or false, so a client reads a field rather than an absence. When true, `destinationMasked` and `alreadySent` are present and your UI should collect the code and post it to `POST /care/chats/{chatId}/verifications/confirm`. A verification that is outstanding is never reported as a `401`: on this surface a `401` means the platform credential is wrong, or a verified chat was driven without its session token.' + description: |- + Whether the cardholder still has to prove they are present before the agent can reach their account. Present on every response, true or false, so a client reads a field rather than an absence. When true, `destinationMasked` and `alreadySent` are present and your UI should collect the code and post it to `POST /care/chats/{chatId}/verifications/confirm`. A verification that is outstanding is never reported as a `401`: on this surface a `401` means the platform credential is wrong, or a chat whose verification is still current was driven without its session token. + + Read this field rather than `verificationStatus`. A chat whose verification has aged past the session lifetime reports `verificationStatus: VERIFIED` and `verificationRequired: true` at the same time, and that combination is the truthful one: the column records that a code was once answered, and this field records whether the agent can act on the account now. + + It describes the chat as of the moment the turn started, so the one turn in which the cardholder read their code back to the agent 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. example: true id: type: string @@ -27969,6 +28058,22 @@ components: type: integer description: Number of digits in the code, read from the verification service's own configuration. Absent when it could not be read — leave the input unbounded in that case rather than guessing, since a guess that disagrees with the service makes every correct code look like a typo. example: 6 + sessionToken: + type: string + description: |- + The session token for this chat, present only on the turn in which the cardholder verified themselves inside the conversation, and absent on every other turn. + + A cardholder can read their code back as a message rather than into your code input. The agent puts it to the same provider, through the same check `POST /care/chats/{chatId}/verifications/confirm` performs, and the chat comes out of that turn able to reach the account. This field is how the partner is handed the credential that turn earned. Without it you would hold a chat that is privileged and unusable at once, because every later message on it is refused with `CARE_SESSION_EXPIRED`. + + It is the same credential, under the same field name and with the same meaning, as the one `POST /care/chats/{chatId}/verifications/confirm` returns, so one handler in your app can serve both doors. Send it as the `X-Ls-Care-Session` header from the next call onwards, and treat it the way you treat the confirm route's: returned once, never re-readable, never logged. + + 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. + example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl + expiresAt: + type: string + format: date-time + description: ISO 8601 timestamp, in UTC, after which the `sessionToken` beside it is refused with `CARE_SESSION_EXPIRED`. Present exactly when `sessionToken` is present. Read it rather than assuming a duration. + example: '2026-09-12T14:25:00Z' CareError: type: object description: | @@ -27996,12 +28101,12 @@ components: |------------|------|-------------| | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `customerId` that is not a `Customer` id, a code of the wrong shape, or a chat that cannot be verified at all. | | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused at the content boundary and no turn ran. `reason` carries the sentence to show them. | - | `CARE_VERIFICATION_FAILED` | 400 | The code did not match, or the chat's existing verification is too old to mint another session. Send a new code and confirm it. | + | `CARE_VERIFICATION_FAILED` | 400 | The code did not match, or the chat's existing verification is older than the session lifetime and so cannot mint another session. Send a new code and confirm it, or send the next message and let the agent ask for one. | | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified — that is signalled in a `200` body. | - | `CARE_SESSION_EXPIRED` | 401 | The chat is verified, so it requires `X-Ls-Care-Session`, and the header was missing, malformed, or past its expiry. Send the cardholder a new code and confirm it. | + | `CARE_SESSION_EXPIRED` | 401 | The chat's verification is still current, so it requires `X-Ls-Care-Session`, and the header was missing; or the token presented was malformed or past its expiry. Send the cardholder a new code and confirm it. Once the chat's own verification has aged past the session lifetime this error stops: the header is no longer required, and the next message sends a fresh code instead of being refused. | | `FORBIDDEN` | 403 | Embedded care is not enabled for this platform. | | `CARE_SESSION_PLATFORM_MISMATCH` | 403 | The session token was minted for a different platform. Stop; retrying will not help. | - | `NOT_FOUND` | 404 | No such chat for this platform. A chat belonging to another platform, a chat that never existed, and a malformed chat id are one answer. | + | `NOT_FOUND` | 404 | No such chat for this platform. A chat belonging to another platform, a chat that never existed, a malformed chat id, and an `X-Ls-Care-Session` token minted for a different chat or a different cardholder are one answer. | | `CUSTOMER_NOT_FOUND` | 404 | No such cardholder for this platform. | | `CONFLICT` | 409 | The `Idempotency-Key` was already used for a different message or a different chat — use a new key — or the chat has no cardholder bound to it. | | `CARE_CHAT_ENDED` | 409 | The chat has been closed. Open a new one by sending an empty message to the cardholder. | @@ -28086,7 +28191,7 @@ components: example: '482913' CareChatSession: type: object - description: Proof that the cardholder answered their code, as a signed token the partner sends back on every later message. + description: Proof that the cardholder answered their code, as a signed token the partner sends back on every later message. The message route returns the same two fields, under the same names, on the turn in which the cardholder read their code back to the agent instead of into a code input. required: - status - sessionToken @@ -28105,12 +28210,17 @@ components: **It is returned here and never again.** There is no route that reads it back, and there is no revocation: the short expiry is what makes that acceptable. Hold it in the cardholder's session on your side for as long as they are chatting, and treat losing it as re-verification — send a new code with `POST /care/chats/{chatId}/verifications` and confirm it again. 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. A later message on a chat that is already verified returns no new token, and the one you hold stays the right one until `expiresAt`. + The token is bound to this platform, this chat and this cardholder, so it cannot drive another chat and does not survive being handed to another platform. example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl expiresAt: type: string format: date-time - description: ISO 8601 timestamp, in UTC, after which the token is refused with `CARE_SESSION_EXPIRED`. Sessions are short-lived by design — fifteen minutes in the default deployment. Read this field rather than assuming a duration. + description: |- + ISO 8601 timestamp, in UTC, after which the token is refused with `CARE_SESSION_EXPIRED`. Sessions are short-lived by design — fifteen minutes in the default deployment. Read this field rather than assuming a duration. + + The same window governs the chat itself: a verification older than the session lifetime no longer reaches the cardholder's account, so a chat whose token has expired is not a chat waiting for a token. It is a chat that needs a new code, and the next message on it will send one and have the agent ask for it. example: '2026-09-12T14:25:00Z' WebhookType: type: string diff --git a/openapi.yaml b/openapi.yaml index 2c9e0d8a4..18a918e34 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -88,7 +88,10 @@ tags: 3. **Confirm the code.** `POST /care/chats/{chatId}/verifications/confirm` returns a `sessionToken` and its `expiresAt`. Send that token as the - `X-Ls-Care-Session` header on every message from then on. + `X-Ls-Care-Session` header on every message from then on. A + cardholder who reads the code back to the agent as an ordinary + message instead is confirmed the same way, and that turn's own + response carries the same two fields. 4. **Close the chat** with `DELETE /care/chats/{chatId}` when the cardholder is done. @@ -104,8 +107,21 @@ tags: reported inside a `200`, because the first message from every new cardholder is unverified and a `401` there would be indistinguishable from a bad credential. On these endpoints a `401` means the platform's - Grid credential is wrong, or a verified chat was driven without its - session token. + Grid credential is wrong, or a chat whose verification is still current + was driven without its session token. + + **Verification expires, and expiry is recoverable in the chat.** 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 also how long a session token lives. Past that window the chat + stops requiring the header: a message sent without one gets a fresh + code, and the agent asks the cardholder to read it back inside the + conversation, which hands the partner the next session token. Sending + the dead token anyway is still a `401 CARE_SESSION_EXPIRED`, so the + handling is to discard the token on that error and resend the message + with no `X-Ls-Care-Session` header rather than to tear the chat down. + If that resend is refused too, the chat's verification is still current + and the way back is a new code. Two things an integration has to get right: @@ -117,7 +133,9 @@ tags: - **The session token is returned once and is never re-readable.** No endpoint hands it back, there is no revocation, and it is short-lived by design. Hold it for the life of the cardholder's session; a partner - that drops it has to send a new code and confirm it again. + that drops it has to send a new code and confirm it again. It arrives + from whichever door took the code, so read `sessionToken` on the + message response as well as on the confirm response. Some things are absent by construction, so no integration can come to depend on them: nothing returns a transcript or an earlier turn's reply @@ -10684,16 +10702,59 @@ paths: own input box instead has put an unmasked PAN in the transcript, on the screen and in whatever logs the transcript reaches. - Once the chat is verified every message must carry the session token from - `POST /care/chats/{chatId}/verifications/confirm` in the - `X-Ls-Care-Session` header. Without it the call is refused with - `CARE_SESSION_EXPIRED`: the chat's verified state never expires on its - own, so the session is what proves the cardholder is still at the keyboard. + **The code can also be read back to the agent, and then this route issues + the session.** When the cardholder types their code as a message rather + than into your code input, the agent puts it to the same provider, through + the same check `POST /care/chats/{chatId}/verifications/confirm` performs, + and the chat comes out of that turn able to reach the account. That turn's + body carries `sessionToken` and `expiresAt`, under the same names and with + the same meaning as the confirm route's, so one handler in your app serves + both doors. 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, or you will hold a chat that is privileged and unusable at + once. + + The structured route is not deprecated by this, and it is still the one to + call when your own UI collects the code: only it can tell you where the + code went, whether one was already in flight, and how many digits it has. + + While the chat's verification is current, every message must carry the + session token in the `X-Ls-Care-Session` header, whichever of the two + doors issued it. Without it the call is refused with + `CARE_SESSION_EXPIRED`. + + **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 chat no longer reaches the account, the header stops + being required, and the next message **sent without it** is treated as an + unverified one: a fresh code goes out, the body carries + `verificationRequired: true` with `destinationMasked`, `alreadySent` and + `codeLength`, and the agent's own `reply` asks the cardholder to read the + code back. The turn still runs and still answers. + + Sending the expired token anyway is still a `401 CARE_SESSION_EXPIRED`. + One recovery covers a token that was lost, one that was never valid and + one that has expired: retry the message with no `X-Ls-Care-Session` + header. On a chat whose verification has gone stale that call succeeds, + sends a fresh code and has the agent ask for it, and the turn in which the + cardholder reads it back returns the next `sessionToken`. On a chat whose + verification is still current it is refused with `CARE_SESSION_EXPIRED` + again, which is the signal to send a code with + `POST /care/chats/{chatId}/verifications` and confirm it. 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 describes the chat as of the moment the turn started, `verificationStatus` included. The agent can verify a cardholder during a turn, so a chat that this very call verified still reads - unverified here and verified on the next call. + unverified here and verified on the next call. For the same reason a chat + whose verification has gone stale reports `verificationStatus: VERIFIED` + beside `verificationRequired: true`. Branch on `verificationRequired`, + never on `verificationStatus`. operationId: sendCareChatMessage tags: - Embedded Care @@ -10711,7 +10772,7 @@ paths: - name: X-Ls-Care-Session in: header required: false - description: The `sessionToken` from `POST /care/chats/{chatId}/verifications/confirm`. Required on every message once the chat is verified, and refused with `CARE_SESSION_EXPIRED` when it is missing, malformed or past its expiry. Not read before verification, and not read on an opening — a widget that reloaded has lost its token, which is the case the opening exists for. + description: The `sessionToken` from `POST /care/chats/{chatId}/verifications/confirm`, or the one this route returned on the turn that verified the cardholder in the conversation. Required on every message while the chat's verification is current, and refused with `CARE_SESSION_EXPIRED` when it is missing there. Not required before the cardholder has verified, nor once the chat's verification has aged past the session lifetime; but a token that is sent is always checked, so a malformed or expired one is refused with `CARE_SESSION_EXPIRED` whatever state the chat is in, and one minted for another chat or another cardholder is `NOT_FOUND`. An opening never reads the header at all, because a widget that reloaded has lost its token and that is the case the opening exists for. schema: type: string example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl @@ -10748,7 +10809,7 @@ paths: schema: $ref: '#/components/schemas/CareError' '401': - description: '`UNAUTHORIZED` when the platform''s Grid credential is missing or wrong. `CARE_SESSION_EXPIRED` when the chat is verified and the `X-Ls-Care-Session` header is missing, malformed or expired. An unverified cardholder is never a `401` — that is a `200` carrying `verificationRequired`.' + description: '`UNAUTHORIZED` when the platform''s Grid credential is missing or wrong. `CARE_SESSION_EXPIRED` when the chat''s verification is still current and the `X-Ls-Care-Session` header is missing, or when the token presented is malformed or past its expiry. An unverified cardholder is never a `401`; that is a `200` carrying `verificationRequired`. Neither is a chat whose verification has gone stale, which is asked for a code again inside a `200`.' content: application/json: schema: @@ -10760,13 +10821,13 @@ paths: schema: $ref: '#/components/schemas/CareError' '404': - description: '`NOT_FOUND` when the path names a chat this platform does not own, and `CUSTOMER_NOT_FOUND` when it names a cardholder this platform does not own. A resource that belongs to somebody else and one that never existed are the same answer.' + description: '`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.' content: application/json: schema: $ref: '#/components/schemas/CareError' '409': - description: '`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 the chat has been closed; open a new one by sending an empty message to the cardholder.' + description: '`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 carrying text is sent to a chat that has been closed; open a new one by sending a message addressed to the cardholder as `Customer:`, which finds no open chat and opens a fresh one.' content: application/json: schema: @@ -10824,7 +10885,9 @@ paths: **This is the resend, not the first send.** The cardholder's first message already sent a code and said so in its own body; this is what the *Send another code* button calls when they say it never arrived. A partner never - has to call it to start verification. + has to call it to start verification, and does not have to call it to + recover a chat whose verification has gone stale either, because the next + message sends a code by itself. With `resend` false — the default, and the same as sending no body — the call asks for a code and is content to be told one is already outstanding: @@ -10842,8 +10905,10 @@ paths: A chat that is already verified is refused with `INVALID_INPUT` while its verification is recent — there is nothing left to prove. Once that - verification has aged past the session lifetime the refusal lifts and a - fresh code can be sent. + verification has aged past the session lifetime the chat no longer reaches + the cardholder's account, so there is something to prove again: the + refusal lifts on its own and this route sends a fresh code, whatever + `verificationStatus` still says. operationId: resendCareChatVerification tags: - Embedded Care @@ -10948,6 +11013,13 @@ paths: `POST /care/chats/{chatId}/verifications` and confirm it again. Read `expiresAt` rather than assuming a lifetime. + **This is not the only door.** A cardholder can read the code back to the + agent as an ordinary message, and `POST /care/chats/{chatId}/messages` + then returns the same `sessionToken` and `expiresAt` on that turn. Call + this route when your own UI collects the code, because only it can tell + you where the code went, whether one was already in flight, and how many + digits it has. + The token is bound to this platform, this chat and this cardholder. It cannot drive another chat, and presenting it to another platform's chat is refused with `CARE_SESSION_PLATFORM_MISMATCH`. @@ -10961,8 +11033,12 @@ paths: A chat whose verification is older than the session lifetime will not mint a fresh session off it: that would be the platform issuing itself a session for a cardholder who has not shown themselves in longer than a - session lasts. It is refused with `CARE_VERIFICATION_FAILED`, and the way - forward is a new code. + session lasts. It is refused with `CARE_VERIFICATION_FAILED` before the + provider is reached, and the way forward is a new code. That is the same + window that decides whether the chat reaches the cardholder's account at + all, so a stale chat is not a locked one: sending a code with + `POST /care/chats/{chatId}/verifications`, or simply sending the next + message and letting the agent ask, both open it again. operationId: confirmCareChatVerification tags: - Embedded Care @@ -27883,10 +27959,18 @@ components: - FAILED description: | Where the cardholder's proof of presence stands on this chat, as of the - moment the call was received. A chat in `VERIFIED` or `TRUSTED` can reach - the cardholder's account, and every message on it must carry the - `X-Ls-Care-Session` header; a chat in any other status cannot, and the - agent answers general questions only. + moment the call was received. + + **This status alone does not say whether the agent can reach the account.** + A chat reaches it only while it is in `VERIFIED` or `TRUSTED` *and* the + verification behind that status is newer than the session lifetime, fifteen + minutes in the default deployment. Once it ages past that window the status + still reads `VERIFIED`, the chat no longer reaches the account, the + `X-Ls-Care-Session` header stops being required, and the next message sends + a fresh code and has the agent ask for it. The field that answers "can the + agent act now" is `verificationRequired` on the message response, so branch + on that and treat this status as a record of what has happened rather than + of what is permitted. This field reports the chat as it was when the turn started. The agent can verify a cardholder during a turn, so a chat that was verified by the turn @@ -27897,8 +27981,8 @@ components: |--------|-------------| | `PENDING` | No code has been sent on this chat yet. This is where every new chat starts. | | `SENT` | A code has been sent to the cardholder and is outstanding. | - | `VERIFIED` | The cardholder answered the code. Account-reaching tools are available and `POST /care/chats/{chatId}/verifications/confirm` has issued a session token. | - | `TRUSTED` | The cardholder is treated as present without an SMS code on this chat. Reserved for channels Grid can already attribute; a partner chat does not start here. | + | `VERIFIED` | The cardholder answered the code, either at `POST /care/chats/{chatId}/verifications/confirm` or by reading it back to the agent. Account-reaching tools are available for as long as that answer is newer than the session lifetime, and a session token was issued by whichever of the two doors took the code. | + | `TRUSTED` | The cardholder is treated as present without an SMS code on this chat. Reserved for channels Grid can already attribute; a partner chat does not start here. Bounded by the same freshness window as `VERIFIED`. | | `FAILED` | The last code was answered incorrectly or expired. Send another code with `POST /care/chats/{chatId}/verifications`. | CareChatGreeting: type: object @@ -27923,7 +28007,7 @@ components: example: '2026-09-12T14:10:00.123456Z' CareChatMessageResponse: type: object - description: 'The result of one call to the message route. Which fields are present depends on what the call did: a turn adds `id`, `displayMessage`, `reply` and `replayed`; the call that opened the chat adds `greeting`; an outstanding verification adds `destinationMasked`, `alreadySent` and, where the service could be read, `codeLength`.' + description: 'The result of one call to the message route. Which fields are present depends on what the call did: a turn adds `id`, `displayMessage`, `reply` and `replayed`; the call that opened the chat adds `greeting`; an outstanding verification adds `destinationMasked`, `alreadySent` and, where the service could be read, `codeLength`; and a turn in which the cardholder read their code back to the agent adds `sessionToken` and `expiresAt`.' required: - chatId - verificationStatus @@ -27937,7 +28021,12 @@ components: $ref: '#/components/schemas/CareChatVerificationStatus' verificationRequired: type: boolean - description: 'Whether the cardholder still has to prove they are present before the agent can reach their account. Present on every response, true or false, so a client reads a field rather than an absence. When true, `destinationMasked` and `alreadySent` are present and your UI should collect the code and post it to `POST /care/chats/{chatId}/verifications/confirm`. A verification that is outstanding is never reported as a `401`: on this surface a `401` means the platform credential is wrong, or a verified chat was driven without its session token.' + description: |- + Whether the cardholder still has to prove they are present before the agent can reach their account. Present on every response, true or false, so a client reads a field rather than an absence. When true, `destinationMasked` and `alreadySent` are present and your UI should collect the code and post it to `POST /care/chats/{chatId}/verifications/confirm`. A verification that is outstanding is never reported as a `401`: on this surface a `401` means the platform credential is wrong, or a chat whose verification is still current was driven without its session token. + + Read this field rather than `verificationStatus`. A chat whose verification has aged past the session lifetime reports `verificationStatus: VERIFIED` and `verificationRequired: true` at the same time, and that combination is the truthful one: the column records that a code was once answered, and this field records whether the agent can act on the account now. + + It describes the chat as of the moment the turn started, so the one turn in which the cardholder read their code back to the agent 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. example: true id: type: string @@ -27969,6 +28058,22 @@ components: type: integer description: Number of digits in the code, read from the verification service's own configuration. Absent when it could not be read — leave the input unbounded in that case rather than guessing, since a guess that disagrees with the service makes every correct code look like a typo. example: 6 + sessionToken: + type: string + description: |- + The session token for this chat, present only on the turn in which the cardholder verified themselves inside the conversation, and absent on every other turn. + + A cardholder can read their code back as a message rather than into your code input. The agent puts it to the same provider, through the same check `POST /care/chats/{chatId}/verifications/confirm` performs, and the chat comes out of that turn able to reach the account. This field is how the partner is handed the credential that turn earned. Without it you would hold a chat that is privileged and unusable at once, because every later message on it is refused with `CARE_SESSION_EXPIRED`. + + It is the same credential, under the same field name and with the same meaning, as the one `POST /care/chats/{chatId}/verifications/confirm` returns, so one handler in your app can serve both doors. Send it as the `X-Ls-Care-Session` header from the next call onwards, and treat it the way you treat the confirm route's: returned once, never re-readable, never logged. + + 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. + example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl + expiresAt: + type: string + format: date-time + description: ISO 8601 timestamp, in UTC, after which the `sessionToken` beside it is refused with `CARE_SESSION_EXPIRED`. Present exactly when `sessionToken` is present. Read it rather than assuming a duration. + example: '2026-09-12T14:25:00Z' CareError: type: object description: | @@ -27996,12 +28101,12 @@ components: |------------|------|-------------| | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `customerId` that is not a `Customer` id, a code of the wrong shape, or a chat that cannot be verified at all. | | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused at the content boundary and no turn ran. `reason` carries the sentence to show them. | - | `CARE_VERIFICATION_FAILED` | 400 | The code did not match, or the chat's existing verification is too old to mint another session. Send a new code and confirm it. | + | `CARE_VERIFICATION_FAILED` | 400 | The code did not match, or the chat's existing verification is older than the session lifetime and so cannot mint another session. Send a new code and confirm it, or send the next message and let the agent ask for one. | | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified — that is signalled in a `200` body. | - | `CARE_SESSION_EXPIRED` | 401 | The chat is verified, so it requires `X-Ls-Care-Session`, and the header was missing, malformed, or past its expiry. Send the cardholder a new code and confirm it. | + | `CARE_SESSION_EXPIRED` | 401 | The chat's verification is still current, so it requires `X-Ls-Care-Session`, and the header was missing; or the token presented was malformed or past its expiry. Send the cardholder a new code and confirm it. Once the chat's own verification has aged past the session lifetime this error stops: the header is no longer required, and the next message sends a fresh code instead of being refused. | | `FORBIDDEN` | 403 | Embedded care is not enabled for this platform. | | `CARE_SESSION_PLATFORM_MISMATCH` | 403 | The session token was minted for a different platform. Stop; retrying will not help. | - | `NOT_FOUND` | 404 | No such chat for this platform. A chat belonging to another platform, a chat that never existed, and a malformed chat id are one answer. | + | `NOT_FOUND` | 404 | No such chat for this platform. A chat belonging to another platform, a chat that never existed, a malformed chat id, and an `X-Ls-Care-Session` token minted for a different chat or a different cardholder are one answer. | | `CUSTOMER_NOT_FOUND` | 404 | No such cardholder for this platform. | | `CONFLICT` | 409 | The `Idempotency-Key` was already used for a different message or a different chat — use a new key — or the chat has no cardholder bound to it. | | `CARE_CHAT_ENDED` | 409 | The chat has been closed. Open a new one by sending an empty message to the cardholder. | @@ -28086,7 +28191,7 @@ components: example: '482913' CareChatSession: type: object - description: Proof that the cardholder answered their code, as a signed token the partner sends back on every later message. + description: Proof that the cardholder answered their code, as a signed token the partner sends back on every later message. The message route returns the same two fields, under the same names, on the turn in which the cardholder read their code back to the agent instead of into a code input. required: - status - sessionToken @@ -28105,12 +28210,17 @@ components: **It is returned here and never again.** There is no route that reads it back, and there is no revocation: the short expiry is what makes that acceptable. Hold it in the cardholder's session on your side for as long as they are chatting, and treat losing it as re-verification — send a new code with `POST /care/chats/{chatId}/verifications` and confirm it again. 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. A later message on a chat that is already verified returns no new token, and the one you hold stays the right one until `expiresAt`. + The token is bound to this platform, this chat and this cardholder, so it cannot drive another chat and does not survive being handed to another platform. example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl expiresAt: type: string format: date-time - description: ISO 8601 timestamp, in UTC, after which the token is refused with `CARE_SESSION_EXPIRED`. Sessions are short-lived by design — fifteen minutes in the default deployment. Read this field rather than assuming a duration. + description: |- + ISO 8601 timestamp, in UTC, after which the token is refused with `CARE_SESSION_EXPIRED`. Sessions are short-lived by design — fifteen minutes in the default deployment. Read this field rather than assuming a duration. + + The same window governs the chat itself: a verification older than the session lifetime no longer reaches the cardholder's account, so a chat whose token has expired is not a chat waiting for a token. It is a chat that needs a new code, and the next message on it will send one and have the agent ask for it. example: '2026-09-12T14:25:00Z' WebhookType: type: string diff --git a/openapi/components/schemas/care/CareChatMessageResponse.yaml b/openapi/components/schemas/care/CareChatMessageResponse.yaml index 0b0373810..b7df00b07 100644 --- a/openapi/components/schemas/care/CareChatMessageResponse.yaml +++ b/openapi/components/schemas/care/CareChatMessageResponse.yaml @@ -4,7 +4,9 @@ description: >- depends on what the call did: a turn adds `id`, `displayMessage`, `reply` and `replayed`; the call that opened the chat adds `greeting`; an outstanding verification adds `destinationMasked`, `alreadySent` and, - where the service could be read, `codeLength`. + where the service could be read, `codeLength`; and a turn in which the + cardholder read their code back to the agent adds `sessionToken` and + `expiresAt`. required: - chatId - verificationStatus @@ -30,8 +32,23 @@ properties: collect the code and post it to `POST /care/chats/{chatId}/verifications/confirm`. A verification that is outstanding is never reported as a `401`: on this surface a `401` - means the platform credential is wrong, or a verified chat was driven - without its session token. + means the platform credential is wrong, or a chat whose verification is + still current was driven without its session token. + + + Read this field rather than `verificationStatus`. A chat whose + verification has aged past the session lifetime reports + `verificationStatus: VERIFIED` and `verificationRequired: true` at the + same time, and that combination is the truthful one: the column records + that a code was once answered, and this field records whether the agent + can act on the account now. + + + It describes the chat as of the moment the turn started, so the one turn + in which the cardholder read their code back to the agent 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. example: true id: type: string @@ -92,3 +109,41 @@ properties: unbounded in that case rather than guessing, since a guess that disagrees with the service makes every correct code look like a typo. example: 6 + sessionToken: + type: string + description: >- + The session token for this chat, present only on the turn in which the + cardholder verified themselves inside the conversation, and absent on + every other turn. + + + A cardholder can read their code back as a message rather than into + your code input. The agent puts it to the same provider, through the + same check `POST /care/chats/{chatId}/verifications/confirm` performs, + and the chat comes out of that turn able to reach the account. + This field is how the partner is handed the credential that turn earned. + Without it you would hold a chat that is privileged and unusable at + once, because every later message on it is refused with + `CARE_SESSION_EXPIRED`. + + + It is the same credential, under the same field name and with the same + meaning, as the one `POST /care/chats/{chatId}/verifications/confirm` + returns, so one handler in your app can serve both doors. Send it as the + `X-Ls-Care-Session` header from the next call onwards, and treat it the + way you treat the confirm route's: returned once, never re-readable, + never logged. + + + 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. + example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl + expiresAt: + type: string + format: date-time + description: >- + ISO 8601 timestamp, in UTC, after which the `sessionToken` beside it is + refused with `CARE_SESSION_EXPIRED`. Present exactly when + `sessionToken` is present. Read it rather than assuming a duration. + example: '2026-09-12T14:25:00Z' diff --git a/openapi/components/schemas/care/CareChatSession.yaml b/openapi/components/schemas/care/CareChatSession.yaml index d251cd8d8..2ea707681 100644 --- a/openapi/components/schemas/care/CareChatSession.yaml +++ b/openapi/components/schemas/care/CareChatSession.yaml @@ -1,7 +1,9 @@ type: object description: >- Proof that the cardholder answered their code, as a signed token the - partner sends back on every later message. + partner sends back on every later message. The message route returns the + same two fields, under the same names, on the turn in which the cardholder + read their code back to the agent instead of into a code input. required: - status - sessionToken @@ -29,6 +31,11 @@ properties: it past `expiresAt`. + One session is issued per verification rather than per message. A later + message on a chat that is already verified returns no new token, and the + one you hold stays the right one until `expiresAt`. + + The token is bound to this platform, this chat and this cardholder, so it cannot drive another chat and does not survive being handed to another platform. @@ -41,4 +48,11 @@ properties: `CARE_SESSION_EXPIRED`. Sessions are short-lived by design — fifteen minutes in the default deployment. Read this field rather than assuming a duration. + + + The same window governs the chat itself: a verification older than the + session lifetime no longer reaches the cardholder's account, so a chat + whose token has expired is not a chat waiting for a token. It is a chat + that needs a new code, and the next message on it will send one and have + the agent ask for it. example: '2026-09-12T14:25:00Z' diff --git a/openapi/components/schemas/care/CareChatVerificationStatus.yaml b/openapi/components/schemas/care/CareChatVerificationStatus.yaml index 177c6e536..2462369df 100644 --- a/openapi/components/schemas/care/CareChatVerificationStatus.yaml +++ b/openapi/components/schemas/care/CareChatVerificationStatus.yaml @@ -7,10 +7,18 @@ enum: - FAILED description: | Where the cardholder's proof of presence stands on this chat, as of the - moment the call was received. A chat in `VERIFIED` or `TRUSTED` can reach - the cardholder's account, and every message on it must carry the - `X-Ls-Care-Session` header; a chat in any other status cannot, and the - agent answers general questions only. + moment the call was received. + + **This status alone does not say whether the agent can reach the account.** + A chat reaches it only while it is in `VERIFIED` or `TRUSTED` *and* the + verification behind that status is newer than the session lifetime, fifteen + minutes in the default deployment. Once it ages past that window the status + still reads `VERIFIED`, the chat no longer reaches the account, the + `X-Ls-Care-Session` header stops being required, and the next message sends + a fresh code and has the agent ask for it. The field that answers "can the + agent act now" is `verificationRequired` on the message response, so branch + on that and treat this status as a record of what has happened rather than + of what is permitted. This field reports the chat as it was when the turn started. The agent can verify a cardholder during a turn, so a chat that was verified by the turn @@ -21,6 +29,6 @@ description: | |--------|-------------| | `PENDING` | No code has been sent on this chat yet. This is where every new chat starts. | | `SENT` | A code has been sent to the cardholder and is outstanding. | - | `VERIFIED` | The cardholder answered the code. Account-reaching tools are available and `POST /care/chats/{chatId}/verifications/confirm` has issued a session token. | - | `TRUSTED` | The cardholder is treated as present without an SMS code on this chat. Reserved for channels Grid can already attribute; a partner chat does not start here. | + | `VERIFIED` | The cardholder answered the code, either at `POST /care/chats/{chatId}/verifications/confirm` or by reading it back to the agent. Account-reaching tools are available for as long as that answer is newer than the session lifetime, and a session token was issued by whichever of the two doors took the code. | + | `TRUSTED` | The cardholder is treated as present without an SMS code on this chat. Reserved for channels Grid can already attribute; a partner chat does not start here. Bounded by the same freshness window as `VERIFIED`. | | `FAILED` | The last code was answered incorrectly or expired. Send another code with `POST /care/chats/{chatId}/verifications`. | diff --git a/openapi/components/schemas/care/CareError.yaml b/openapi/components/schemas/care/CareError.yaml index a5e222f3c..00c0c959b 100644 --- a/openapi/components/schemas/care/CareError.yaml +++ b/openapi/components/schemas/care/CareError.yaml @@ -24,12 +24,12 @@ properties: |------------|------|-------------| | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `customerId` that is not a `Customer` id, a code of the wrong shape, or a chat that cannot be verified at all. | | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused at the content boundary and no turn ran. `reason` carries the sentence to show them. | - | `CARE_VERIFICATION_FAILED` | 400 | The code did not match, or the chat's existing verification is too old to mint another session. Send a new code and confirm it. | + | `CARE_VERIFICATION_FAILED` | 400 | The code did not match, or the chat's existing verification is older than the session lifetime and so cannot mint another session. Send a new code and confirm it, or send the next message and let the agent ask for one. | | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified — that is signalled in a `200` body. | - | `CARE_SESSION_EXPIRED` | 401 | The chat is verified, so it requires `X-Ls-Care-Session`, and the header was missing, malformed, or past its expiry. Send the cardholder a new code and confirm it. | + | `CARE_SESSION_EXPIRED` | 401 | The chat's verification is still current, so it requires `X-Ls-Care-Session`, and the header was missing; or the token presented was malformed or past its expiry. Send the cardholder a new code and confirm it. Once the chat's own verification has aged past the session lifetime this error stops: the header is no longer required, and the next message sends a fresh code instead of being refused. | | `FORBIDDEN` | 403 | Embedded care is not enabled for this platform. | | `CARE_SESSION_PLATFORM_MISMATCH` | 403 | The session token was minted for a different platform. Stop; retrying will not help. | - | `NOT_FOUND` | 404 | No such chat for this platform. A chat belonging to another platform, a chat that never existed, and a malformed chat id are one answer. | + | `NOT_FOUND` | 404 | No such chat for this platform. A chat belonging to another platform, a chat that never existed, a malformed chat id, and an `X-Ls-Care-Session` token minted for a different chat or a different cardholder are one answer. | | `CUSTOMER_NOT_FOUND` | 404 | No such cardholder for this platform. | | `CONFLICT` | 409 | The `Idempotency-Key` was already used for a different message or a different chat — use a new key — or the chat has no cardholder bound to it. | | `CARE_CHAT_ENDED` | 409 | The chat has been closed. Open a new one by sending an empty message to the cardholder. | diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 880c401b2..bf50ba7c7 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -124,7 +124,10 @@ tags: 3. **Confirm the code.** `POST /care/chats/{chatId}/verifications/confirm` returns a `sessionToken` and its `expiresAt`. Send that token as the - `X-Ls-Care-Session` header on every message from then on. + `X-Ls-Care-Session` header on every message from then on. A + cardholder who reads the code back to the agent as an ordinary + message instead is confirmed the same way, and that turn's own + response carries the same two fields. 4. **Close the chat** with `DELETE /care/chats/{chatId}` when the cardholder is done. @@ -140,8 +143,21 @@ tags: reported inside a `200`, because the first message from every new cardholder is unverified and a `401` there would be indistinguishable from a bad credential. On these endpoints a `401` means the platform's - Grid credential is wrong, or a verified chat was driven without its - session token. + Grid credential is wrong, or a chat whose verification is still current + was driven without its session token. + + **Verification expires, and expiry is recoverable in the chat.** 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 also how long a session token lives. Past that window the chat + stops requiring the header: a message sent without one gets a fresh + code, and the agent asks the cardholder to read it back inside the + conversation, which hands the partner the next session token. Sending + the dead token anyway is still a `401 CARE_SESSION_EXPIRED`, so the + handling is to discard the token on that error and resend the message + with no `X-Ls-Care-Session` header rather than to tear the chat down. + If that resend is refused too, the chat's verification is still current + and the way back is a new code. Two things an integration has to get right: @@ -153,7 +169,9 @@ tags: - **The session token is returned once and is never re-readable.** No endpoint hands it back, there is no revocation, and it is short-lived by design. Hold it for the life of the cardholder's session; a partner - that drops it has to send a new code and confirm it again. + that drops it has to send a new code and confirm it again. It arrives + from whichever door took the code, so read `sessionToken` on the + message response as well as on the confirm response. Some things are absent by construction, so no integration can come to depend on them: nothing returns a transcript or an earlier turn's reply diff --git a/openapi/paths/care/care_chats_{chatId}_messages.yaml b/openapi/paths/care/care_chats_{chatId}_messages.yaml index 971d6e0fa..bc7b369ff 100644 --- a/openapi/paths/care/care_chats_{chatId}_messages.yaml +++ b/openapi/paths/care/care_chats_{chatId}_messages.yaml @@ -57,16 +57,59 @@ post: own input box instead has put an unmasked PAN in the transcript, on the screen and in whatever logs the transcript reaches. - Once the chat is verified every message must carry the session token from - `POST /care/chats/{chatId}/verifications/confirm` in the - `X-Ls-Care-Session` header. Without it the call is refused with - `CARE_SESSION_EXPIRED`: the chat's verified state never expires on its - own, so the session is what proves the cardholder is still at the keyboard. + **The code can also be read back to the agent, and then this route issues + the session.** When the cardholder types their code as a message rather + than into your code input, the agent puts it to the same provider, through + the same check `POST /care/chats/{chatId}/verifications/confirm` performs, + and the chat comes out of that turn able to reach the account. That turn's + body carries `sessionToken` and `expiresAt`, under the same names and with + the same meaning as the confirm route's, so one handler in your app serves + both doors. 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, or you will hold a chat that is privileged and unusable at + once. + + The structured route is not deprecated by this, and it is still the one to + call when your own UI collects the code: only it can tell you where the + code went, whether one was already in flight, and how many digits it has. + + While the chat's verification is current, every message must carry the + session token in the `X-Ls-Care-Session` header, whichever of the two + doors issued it. Without it the call is refused with + `CARE_SESSION_EXPIRED`. + + **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 chat no longer reaches the account, the header stops + being required, and the next message **sent without it** is treated as an + unverified one: a fresh code goes out, the body carries + `verificationRequired: true` with `destinationMasked`, `alreadySent` and + `codeLength`, and the agent's own `reply` asks the cardholder to read the + code back. The turn still runs and still answers. + + Sending the expired token anyway is still a `401 CARE_SESSION_EXPIRED`. + One recovery covers a token that was lost, one that was never valid and + one that has expired: retry the message with no `X-Ls-Care-Session` + header. On a chat whose verification has gone stale that call succeeds, + sends a fresh code and has the agent ask for it, and the turn in which the + cardholder reads it back returns the next `sessionToken`. On a chat whose + verification is still current it is refused with `CARE_SESSION_EXPIRED` + again, which is the signal to send a code with + `POST /care/chats/{chatId}/verifications` and confirm it. 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 describes the chat as of the moment the turn started, `verificationStatus` included. The agent can verify a cardholder during a turn, so a chat that this very call verified still reads - unverified here and verified on the next call. + unverified here and verified on the next call. For the same reason a chat + whose verification has gone stale reports `verificationStatus: VERIFIED` + beside `verificationRequired: true`. Branch on `verificationRequired`, + never on `verificationStatus`. operationId: sendCareChatMessage tags: - Embedded Care @@ -94,12 +137,17 @@ post: required: false description: >- The `sessionToken` from - `POST /care/chats/{chatId}/verifications/confirm`. Required on every - message once the chat is verified, and refused with - `CARE_SESSION_EXPIRED` when it is missing, malformed or past its - expiry. Not read before verification, and not read on an opening — a - widget that reloaded has lost its token, which is the case the opening - exists for. + `POST /care/chats/{chatId}/verifications/confirm`, or the one this + route returned on the turn that verified the cardholder in the + conversation. Required on every message while the chat's verification + is current, and refused with `CARE_SESSION_EXPIRED` when it is missing + there. Not required before the cardholder has verified, nor once the + chat's verification has aged past the session lifetime; but a token + that is sent is always checked, so a malformed or expired one is + refused with `CARE_SESSION_EXPIRED` whatever state the chat is in, and + one minted for another chat or another cardholder is `NOT_FOUND`. An + opening never reads the header at all, because a widget that reloaded + has lost its token and that is the case the opening exists for. schema: type: string example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl @@ -147,10 +195,12 @@ post: '401': description: >- `UNAUTHORIZED` when the platform's Grid credential is missing or - wrong. `CARE_SESSION_EXPIRED` when the chat is verified and the - `X-Ls-Care-Session` header is missing, malformed or expired. An - unverified cardholder is never a `401` — that is a `200` carrying - `verificationRequired`. + wrong. `CARE_SESSION_EXPIRED` when the chat's verification is still + current and the `X-Ls-Care-Session` header is missing, or when the + token presented is malformed or past its expiry. An unverified + cardholder is never a `401`; that is a `200` carrying + `verificationRequired`. Neither is a chat whose verification has gone + stale, which is asked for a code again inside a `200`. content: application/json: schema: @@ -166,10 +216,13 @@ post: $ref: ../../components/schemas/care/CareError.yaml '404': description: >- - `NOT_FOUND` when the path names a chat this platform does not own, and - `CUSTOMER_NOT_FOUND` when it names a cardholder this platform does not - own. A resource that belongs to somebody else and one that never - existed are the same answer. + `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. content: application/json: schema: @@ -178,8 +231,10 @@ post: description: >- `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 the chat has been - closed; open a new one by sending an empty message to the cardholder. + cardholder bound to it. `CARE_CHAT_ENDED` when a message carrying text + is sent to a chat that has been closed; open a new one by sending a + message addressed to the cardholder as `Customer:`, which finds + no open chat and opens a fresh one. content: application/json: schema: diff --git a/openapi/paths/care/care_chats_{chatId}_verifications.yaml b/openapi/paths/care/care_chats_{chatId}_verifications.yaml index b6f4af439..d6e397159 100644 --- a/openapi/paths/care/care_chats_{chatId}_verifications.yaml +++ b/openapi/paths/care/care_chats_{chatId}_verifications.yaml @@ -16,7 +16,9 @@ post: **This is the resend, not the first send.** The cardholder's first message already sent a code and said so in its own body; this is what the *Send another code* button calls when they say it never arrived. A partner never - has to call it to start verification. + has to call it to start verification, and does not have to call it to + recover a chat whose verification has gone stale either, because the next + message sends a code by itself. With `resend` false — the default, and the same as sending no body — the call asks for a code and is content to be told one is already outstanding: @@ -34,8 +36,10 @@ post: A chat that is already verified is refused with `INVALID_INPUT` while its verification is recent — there is nothing left to prove. Once that - verification has aged past the session lifetime the refusal lifts and a - fresh code can be sent. + verification has aged past the session lifetime the chat no longer reaches + the cardholder's account, so there is something to prove again: the + refusal lifts on its own and this route sends a fresh code, whatever + `verificationStatus` still says. operationId: resendCareChatVerification tags: - Embedded Care diff --git a/openapi/paths/care/care_chats_{chatId}_verifications_confirm.yaml b/openapi/paths/care/care_chats_{chatId}_verifications_confirm.yaml index 2bd16e1c8..abb2923cd 100644 --- a/openapi/paths/care/care_chats_{chatId}_verifications_confirm.yaml +++ b/openapi/paths/care/care_chats_{chatId}_verifications_confirm.yaml @@ -22,6 +22,13 @@ post: `POST /care/chats/{chatId}/verifications` and confirm it again. Read `expiresAt` rather than assuming a lifetime. + **This is not the only door.** A cardholder can read the code back to the + agent as an ordinary message, and `POST /care/chats/{chatId}/messages` + then returns the same `sessionToken` and `expiresAt` on that turn. Call + this route when your own UI collects the code, because only it can tell + you where the code went, whether one was already in flight, and how many + digits it has. + The token is bound to this platform, this chat and this cardholder. It cannot drive another chat, and presenting it to another platform's chat is refused with `CARE_SESSION_PLATFORM_MISMATCH`. @@ -35,8 +42,12 @@ post: A chat whose verification is older than the session lifetime will not mint a fresh session off it: that would be the platform issuing itself a session for a cardholder who has not shown themselves in longer than a - session lasts. It is refused with `CARE_VERIFICATION_FAILED`, and the way - forward is a new code. + session lasts. It is refused with `CARE_VERIFICATION_FAILED` before the + provider is reached, and the way forward is a new code. That is the same + window that decides whether the chat reaches the cardholder's account at + all, so a stale chat is not a locked one: sending a code with + `POST /care/chats/{chatId}/verifications`, or simply sending the next + message and letting the agent ask, both open it again. operationId: confirmCareChatVerification tags: - Embedded Care From d54a4e7d7f00fd5eb694479a3682a08d902dbe5c Mon Sep 17 00:00:00 2001 From: kphurley7 Date: Wed, 16 Sep 2026 15:28:12 -0500 Subject: [PATCH 04/11] docs(care): make in-chat verification the only way to verify Removes POST /care/chats/{chatId}/verifications and POST /care/chats/{chatId}/verifications/confirm, with the four schemas only they used (CareChatVerificationRequest, CareChatVerificationResponse, CareChatVerificationConfirmRequest, CareChatSession) and their Stainless subresource. The cardholder completes verification by sending the code as an ordinary message, and the message response carries sessionToken and expiresAt on the turn that verified them. A cardholder whose code never arrived asks the agent for another one. The tag overview, the message route, the message response fields, the verification status table and the error table now say so. CARE_VERIFICATION_FAILED is dropped because no remaining endpoint sends it, and the lost-token recovery now closes the chat and opens a fresh one instead of calling the removed routes. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015njn7Fen4mg9NDX7AZQRTj --- .stainless/stainless.yml | 10 - mintlify/openapi.yaml | 449 +++--------------- openapi.yaml | 449 +++--------------- .../schemas/care/CareChatMessageResponse.yaml | 46 +- .../schemas/care/CareChatSession.yaml | 58 --- .../CareChatVerificationConfirmRequest.yaml | 16 - .../care/CareChatVerificationRequest.yaml | 21 - .../care/CareChatVerificationResponse.yaml | 31 -- .../care/CareChatVerificationStatus.yaml | 4 +- .../components/schemas/care/CareError.yaml | 6 +- openapi/openapi.yaml | 50 +- .../care/care_chats_{chatId}_messages.yaml | 55 ++- .../care_chats_{chatId}_verifications.yaml | 143 ------ ..._chats_{chatId}_verifications_confirm.yaml | 144 ------ 14 files changed, 202 insertions(+), 1280 deletions(-) delete mode 100644 openapi/components/schemas/care/CareChatSession.yaml delete mode 100644 openapi/components/schemas/care/CareChatVerificationConfirmRequest.yaml delete mode 100644 openapi/components/schemas/care/CareChatVerificationRequest.yaml delete mode 100644 openapi/components/schemas/care/CareChatVerificationResponse.yaml delete mode 100644 openapi/paths/care/care_chats_{chatId}_verifications.yaml delete mode 100644 openapi/paths/care/care_chats_{chatId}_verifications_confirm.yaml diff --git a/.stainless/stainless.yml b/.stainless/stainless.yml index 2b98d6e06..7daeea06c 100644 --- a/.stainless/stainless.yml +++ b/.stainless/stainless.yml @@ -694,16 +694,6 @@ resources: care_chat_greeting: "#/components/schemas/CareChatGreeting" care_chat_verification_status: "#/components/schemas/CareChatVerificationStatus" care_error: "#/components/schemas/CareError" - subresources: - verifications: - methods: - send: post /care/chats/{chatId}/verifications - confirm: post /care/chats/{chatId}/verifications/confirm - models: - care_chat_verification_request: "#/components/schemas/CareChatVerificationRequest" - care_chat_verification_response: "#/components/schemas/CareChatVerificationResponse" - care_chat_verification_confirm_request: "#/components/schemas/CareChatVerificationConfirmRequest" - care_chat_session: "#/components/schemas/CareChatSession" settings: # All generated integration tests that hit the prism mock http server are marked diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 18a918e34..1d21fec17 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -67,37 +67,37 @@ tags: from the partner's own app. The platform calls with its ordinary Grid credential, but the person typing is the cardholder rather than the caller, so reaching that cardholder's account takes a second proof: an - SMS code they answer, which the partner afterwards carries in the - `X-Ls-Care-Session` header. + SMS code they send into the chat, after which the partner carries a + session token in the `X-Ls-Care-Session` header. - **The surface is four routes, and the happy path is two calls.** + **The surface is two routes, and verification happens inside the + conversation.** 1. **Open the chat.** `POST /care/chats/{chatId}/messages` with an empty `message`, addressed to the cardholder as `Customer:`. Returns the chat's id and the agent's `greeting`, which is what a chat window renders before anybody has typed. Sends no SMS, runs no turn, bills - nothing, and is safe to call again — a widget that reloaded gets the + nothing, and is safe to call again: a widget that reloaded gets the same chat back, this time without a greeting. 2. **Send the cardholder's first message.** The same route with their text and an `Idempotency-Key` header. One turn runs synchronously and the agent's answer comes back in `reply`. On a chat that cannot yet reach the account this call also texts the verification code and says so in the same body: `verificationRequired: true`, with - `destinationMasked`, `alreadySent` and `codeLength` for the input you - put on screen. - 3. **Confirm the code.** - `POST /care/chats/{chatId}/verifications/confirm` returns a - `sessionToken` and its `expiresAt`. Send that token as the - `X-Ls-Care-Session` header on every message from then on. A - cardholder who reads the code back to the agent as an ordinary - message instead is confirmed the same way, and that turn's own - response carries the same two fields. + `destinationMasked`, `alreadySent` and `codeLength`, and the agent's + `reply` asks the cardholder for the code. + 3. **The cardholder sends the code as an ordinary message.** The agent + checks it with the SMS provider, and the response to that turn + carries `sessionToken` and `expiresAt`. Send that token as the + `X-Ls-Care-Session` header on every message from then on. A code that + does not match is answered in `reply`, and the cardholder can simply + send it again. 4. **Close the chat** with `DELETE /care/chats/{chatId}` when the cardholder is done. - `POST /care/chats/{chatId}/verifications` is the *Send another code* - button rather than a step in the flow: step 2 already sent the first - code. + **A cardholder whose code never arrived asks the agent for another + one.** The agent sends a new code to the number on file and says so in + its `reply`. There is no separate route for it. Step 1 is optional. A message addressed to a `Customer:` id opens that cardholder's chat if they have none, so a partner whose cardholder has @@ -115,13 +115,16 @@ tags: than the session lifetime, fifteen minutes in the default deployment, which is also how long a session token lives. Past that window the chat stops requiring the header: a message sent without one gets a fresh - code, and the agent asks the cardholder to read it back inside the + code, and the agent asks the cardholder to send it back inside the conversation, which hands the partner the next session token. Sending the dead token anyway is still a `401 CARE_SESSION_EXPIRED`, so the handling is to discard the token on that error and resend the message with no `X-Ls-Care-Session` header rather than to tear the chat down. If that resend is refused too, the chat's verification is still current - and the way back is a new code. + and its token has been lost. Close the chat with + `DELETE /care/chats/{chatId}`, which needs no token, and send the + message to the cardholder as `Customer:`: that opens a fresh chat + and sends a new code. Two things an integration has to get right: @@ -132,10 +135,9 @@ tags: and into every log the transcript reaches. - **The session token is returned once and is never re-readable.** No endpoint hands it back, there is no revocation, and it is short-lived - by design. Hold it for the life of the cardholder's session; a partner - that drops it has to send a new code and confirm it again. It arrives - from whichever door took the code, so read `sessionToken` on the - message response as well as on the confirm response. + by design. Hold it for the life of the cardholder's session. It + arrives on the message response to the turn in which the cardholder + sent their code, and on no other. Some things are absent by construction, so no integration can come to depend on them: nothing returns a transcript or an earlier turn's reply @@ -10693,8 +10695,6 @@ paths: `codeLength` come back with it, and the agent's own sentence in `reply` asks the cardholder for the code. Later messages on the same chat find the code already outstanding and send nothing, reporting `alreadySent: true`. - Collect the code and post it to - `POST /care/chats/{chatId}/verifications/confirm`. **Render `displayMessage`, not your own copy of the input.** Grid redacts the cardholder's text on the way in — a card number they pasted comes back @@ -10702,26 +10702,23 @@ paths: own input box instead has put an unmasked PAN in the transcript, on the screen and in whatever logs the transcript reaches. - **The code can also be read back to the agent, and then this route issues - the session.** When the cardholder types their code as a message rather - than into your code input, the agent puts it to the same provider, through - the same check `POST /care/chats/{chatId}/verifications/confirm` performs, - and the chat comes out of that turn able to reach the account. That turn's - body carries `sessionToken` and `expiresAt`, under the same names and with - the same meaning as the confirm route's, so one handler in your app serves - both doors. 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, or you will hold a chat that is privileged and unusable at - once. - - The structured route is not deprecated by this, and it is still the one to - call when your own UI collects the code: only it can tell you where the - code went, whether one was already in flight, and how many digits it has. + **The cardholder completes verification by sending the code as an + ordinary message, and that turn's response carries the session.** The + agent checks the code with the SMS provider, and the chat comes out of + that turn able to reach the account. 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, or you will hold a chat that is privileged and + unusable at once. 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, every message must carry the - session token in the `X-Ls-Care-Session` header, whichever of the two - doors issued it. Without it the call is refused with - `CARE_SESSION_EXPIRED`. + session token in the `X-Ls-Care-Session` header. Without it the call is + refused with `CARE_SESSION_EXPIRED`. **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 @@ -10732,18 +10729,20 @@ paths: being required, and the next message **sent without it** is treated as an unverified one: a fresh code goes out, the body carries `verificationRequired: true` with `destinationMasked`, `alreadySent` and - `codeLength`, and the agent's own `reply` asks the cardholder to read the - code back. The turn still runs and still answers. + `codeLength`, and the agent's own `reply` asks the cardholder to send the + code. The turn still runs and still answers. Sending the expired token anyway is still a `401 CARE_SESSION_EXPIRED`. One recovery covers a token that was lost, one that was never valid and one that has expired: retry the message with no `X-Ls-Care-Session` header. On a chat whose verification has gone stale that call succeeds, sends a fresh code and has the agent ask for it, and the turn in which the - cardholder reads it back returns the next `sessionToken`. On a chat whose + cardholder sends it returns the next `sessionToken`. On a chat whose verification is still current it is refused with `CARE_SESSION_EXPIRED` - again, which is the signal to send a code with - `POST /care/chats/{chatId}/verifications` and confirm it. An empty message + again, which means the token for a live verification has been lost: close + the chat with `DELETE /care/chats/{chatId}`, which needs no token, and + send the message to the cardholder as `Customer:`, which opens a + fresh chat and sends a new code. 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. @@ -10772,7 +10771,7 @@ paths: - name: X-Ls-Care-Session in: header required: false - description: The `sessionToken` from `POST /care/chats/{chatId}/verifications/confirm`, or the one this route returned on the turn that verified the cardholder in the conversation. Required on every message while the chat's verification is current, and refused with `CARE_SESSION_EXPIRED` when it is missing there. Not required before the cardholder has verified, nor once the chat's verification has aged past the session lifetime; but a token that is sent is always checked, so a malformed or expired one is refused with `CARE_SESSION_EXPIRED` whatever state the chat is in, and one minted for another chat or another cardholder is `NOT_FOUND`. An opening never reads the header at all, because a widget that reloaded has lost its token and that is the case the opening exists for. + description: The `sessionToken` this route returned on the turn in which the cardholder sent their verification code. Required on every message while the chat's verification is current, and refused with `CARE_SESSION_EXPIRED` when it is missing there. Not required before the cardholder has verified, nor once the chat's verification has aged past the session lifetime; but a token that is sent is always checked, so a malformed or expired one is refused with `CARE_SESSION_EXPIRED` whatever state the chat is in, and one minted for another chat or another cardholder is `NOT_FOUND`. An opening never reads the header at all, because a widget that reloaded has lost its token and that is the case the opening exists for. schema: type: string example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl @@ -10791,6 +10790,10 @@ paths: summary: The cardholder's first message, which also sends the code value: message: My card was declined at the grocery store + verificationCode: + summary: The cardholder sends their code, and this turn returns the session + value: + message: The code is 482913 afterVerification: summary: A later message, sent with the X-Ls-Care-Session header value: @@ -10868,251 +10871,6 @@ paths: application/json: schema: $ref: '#/components/schemas/CareError' - /care/chats/{chatId}/verifications: - parameters: - - name: chatId - in: path - description: The chat to send another code on, as returned in `chatId` by `POST /care/chats/{chatId}/messages`. - required: true - schema: - type: string - example: CareChat:019542f5-b3e7-1d02-0000-000000000030 - post: - summary: Resend a care chat verification code - description: | - Send the cardholder another verification code. - - **This is the resend, not the first send.** The cardholder's first message - already sent a code and said so in its own body; this is what the *Send - another code* button calls when they say it never arrived. A partner never - has to call it to start verification, and does not have to call it to - recover a chat whose verification has gone stale either, because the next - message sends a code by itself. - - With `resend` false — the default, and the same as sending no body — the - call asks for a code and is content to be told one is already outstanding: - the answer carries `alreadySent: true` and no new SMS goes out. That is a - success. With `resend` true a second code is sent. Ask the cardholder - before doing that: codes are sent from a pool of numbers, so the second - arrives from a different sender than the first, leaving them holding two - codes from two strangers and deciding which to read back. - - The destination is always the number already on file for the cardholder. - There is no way to nominate one, because a code sent to a number the - caller chose would prove nothing about the person holding the card. - - `202`, not `200`: the SMS is in flight, not delivered. - - A chat that is already verified is refused with `INVALID_INPUT` while its - verification is recent — there is nothing left to prove. Once that - verification has aged past the session lifetime the chat no longer reaches - the cardholder's account, so there is something to prove again: the - refusal lifts on its own and this route sends a fresh code, whatever - `verificationStatus` still says. - operationId: resendCareChatVerification - tags: - - Embedded Care - security: - - BasicAuth: [] - requestBody: - required: false - content: - application/json: - schema: - $ref: '#/components/schemas/CareChatVerificationRequest' - examples: - askForACode: - summary: Ask for a code, accepting the one already outstanding - value: - resend: false - deliberateResend: - summary: The cardholder says the first code never arrived - value: - resend: true - responses: - '202': - description: The code is on its way, or one was already outstanding and none was sent — `alreadySent` says which. - content: - application/json: - schema: - $ref: '#/components/schemas/CareChatVerificationResponse' - '400': - description: '`INVALID_INPUT` when the chat is already verified, when the body carries an unknown field, or when verification is not available for this chat at all — the platform''s care programme does not offer SMS verification, or the cardholder record behind the chat could not be read.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '401': - description: '`UNAUTHORIZED` — the platform''s Grid credential is missing or wrong.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '403': - description: '`FORBIDDEN` — embedded care is not enabled for this platform.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '404': - description: '`NOT_FOUND` — no such chat for this platform. A chat that belongs to another platform, one that never existed, and a malformed chat id are one answer.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '409': - description: '`CARE_CHAT_ENDED` when the chat has been closed, or `CONFLICT` when it has no cardholder bound to it.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '429': - description: '`CARE_BUDGET_EXCEEDED` when this chat has reached its limit, or `RATE_LIMITED` when the platform is calling this endpoint too often; back off for the interval in the `Retry-After` header.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '500': - description: Internal service error. - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '502': - description: '`CARE_VERIFICATION_UNAVAILABLE` — the SMS provider could not be reached. The factor is down, so retry rather than asking the cardholder for anything.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '503': - description: '`SERVICE_UNAVAILABLE` when embedded care is not available in this deployment, or the support agent is temporarily turned off.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - /care/chats/{chatId}/verifications/confirm: - parameters: - - name: chatId - in: path - description: The chat the code was sent on, as returned in `chatId` by `POST /care/chats/{chatId}/messages`. - required: true - schema: - type: string - example: CareChat:019542f5-b3e7-1d02-0000-000000000030 - post: - summary: Confirm a care chat verification code - description: | - Check the code the cardholder read back, and hand over the session that - every later message on this chat rides on. - - **The session token is returned here and never again.** No endpoint reads - it back, and there is no revocation — the short expiry is what makes that - acceptable. Keep it in the cardholder's own session on your side, send it - as the `X-Ls-Care-Session` header on every later message, and treat losing - it as re-verification: send a new code with - `POST /care/chats/{chatId}/verifications` and confirm it again. Read - `expiresAt` rather than assuming a lifetime. - - **This is not the only door.** A cardholder can read the code back to the - agent as an ordinary message, and `POST /care/chats/{chatId}/messages` - then returns the same `sessionToken` and `expiresAt` on that turn. Call - this route when your own UI collects the code, because only it can tell - you where the code went, whether one was already in flight, and how many - digits it has. - - The token is bound to this platform, this chat and this cardholder. It - cannot drive another chat, and presenting it to another platform's chat is - refused with `CARE_SESSION_PLATFORM_MISMATCH`. - - A code that does not match is `CARE_VERIFICATION_FAILED` — ask the - cardholder again. A provider that could not be reached is - `CARE_VERIFICATION_UNAVAILABLE` — retry; nothing the cardholder does will - help. The two are kept apart deliberately, because they call for opposite - reactions. - - A chat whose verification is older than the session lifetime will not mint - a fresh session off it: that would be the platform issuing itself a - session for a cardholder who has not shown themselves in longer than a - session lasts. It is refused with `CARE_VERIFICATION_FAILED` before the - provider is reached, and the way forward is a new code. That is the same - window that decides whether the chat reaches the cardholder's account at - all, so a stale chat is not a locked one: sending a code with - `POST /care/chats/{chatId}/verifications`, or simply sending the next - message and letting the agent ask, both open it again. - operationId: confirmCareChatVerification - tags: - - Embedded Care - security: - - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CareChatVerificationConfirmRequest' - example: - code: '482913' - responses: - '200': - description: The cardholder is verified on this chat, and the session token is in this body — it is not retrievable afterwards. - content: - application/json: - schema: - $ref: '#/components/schemas/CareChatSession' - '400': - description: '`CARE_VERIFICATION_FAILED` when the code did not match, or when the chat''s existing verification is too old to mint another session. `INVALID_INPUT` when the code is not of the shape the SMS carries, when the body is missing or carries an unknown field, or when verification is not available for this chat.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '401': - description: '`UNAUTHORIZED` — the platform''s Grid credential is missing or wrong.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '403': - description: '`FORBIDDEN` — embedded care is not enabled for this platform.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '404': - description: '`NOT_FOUND` — no such chat for this platform. A chat that belongs to another platform, one that never existed, and a malformed chat id are one answer.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '409': - description: '`CARE_CHAT_ENDED` when the chat has been closed, or `CONFLICT` when it has no cardholder bound to it.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '429': - description: '`CARE_BUDGET_EXCEEDED` when this chat has reached its limit, or `RATE_LIMITED` when the platform is calling this endpoint too often; back off for the interval in the `Retry-After` header.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '500': - description: Internal service error. - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '502': - description: '`CARE_VERIFICATION_UNAVAILABLE` — the SMS provider could not be reached, so the code could not be checked. Retry; the cardholder has done nothing wrong.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '503': - description: '`SERVICE_UNAVAILABLE` when embedded care is not available in this deployment, or the support agent is temporarily turned off.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' /care/chats/{chatId}: parameters: - name: chatId @@ -27981,9 +27739,9 @@ components: |--------|-------------| | `PENDING` | No code has been sent on this chat yet. This is where every new chat starts. | | `SENT` | A code has been sent to the cardholder and is outstanding. | - | `VERIFIED` | The cardholder answered the code, either at `POST /care/chats/{chatId}/verifications/confirm` or by reading it back to the agent. Account-reaching tools are available for as long as that answer is newer than the session lifetime, and a session token was issued by whichever of the two doors took the code. | + | `VERIFIED` | The cardholder sent the code as a message and the agent confirmed it. Account-reaching tools are available for as long as that answer is newer than the session lifetime, and the session token was issued on the message response to that turn. | | `TRUSTED` | The cardholder is treated as present without an SMS code on this chat. Reserved for channels Grid can already attribute; a partner chat does not start here. Bounded by the same freshness window as `VERIFIED`. | - | `FAILED` | The last code was answered incorrectly or expired. Send another code with `POST /care/chats/{chatId}/verifications`. | + | `FAILED` | The last code was answered incorrectly or expired. The cardholder can send the code again, or ask the agent for a new one. | CareChatGreeting: type: object description: 'The agent''s opening line, recorded as the chat''s first message. Returned only by the call that opened the chat, never on a resume: a greeting arriving mid-conversation reads as the agent introducing itself a second time. Absent when the agent produced no opening line.' @@ -28007,7 +27765,7 @@ components: example: '2026-09-12T14:10:00.123456Z' CareChatMessageResponse: type: object - description: 'The result of one call to the message route. Which fields are present depends on what the call did: a turn adds `id`, `displayMessage`, `reply` and `replayed`; the call that opened the chat adds `greeting`; an outstanding verification adds `destinationMasked`, `alreadySent` and, where the service could be read, `codeLength`; and a turn in which the cardholder read their code back to the agent adds `sessionToken` and `expiresAt`.' + description: 'The result of one call to the message route. Which fields are present depends on what the call did: a turn adds `id`, `displayMessage`, `reply` and `replayed`; the call that opened the chat adds `greeting`; an outstanding verification adds `destinationMasked`, `alreadySent` and, where the service could be read, `codeLength`; and the turn in which the cardholder sent their verification code adds `sessionToken` and `expiresAt`.' required: - chatId - verificationStatus @@ -28015,18 +27773,18 @@ components: properties: chatId: type: string - description: 'The chat this call ran on. When the request addressed a cardholder (`Customer:` in the path) this is the chat that was found or opened for them. Use it for every later call on this conversation: confirming the code, resending it, and closing the chat.' + description: 'The chat this call ran on. When the request addressed a cardholder (`Customer:` 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.' example: CareChat:019542f5-b3e7-1d02-0000-000000000030 verificationStatus: $ref: '#/components/schemas/CareChatVerificationStatus' verificationRequired: type: boolean description: |- - Whether the cardholder still has to prove they are present before the agent can reach their account. Present on every response, true or false, so a client reads a field rather than an absence. When true, `destinationMasked` and `alreadySent` are present and your UI should collect the code and post it to `POST /care/chats/{chatId}/verifications/confirm`. A verification that is outstanding is never reported as a `401`: on this surface a `401` means the platform credential is wrong, or a chat whose verification is still current was driven without its session token. + Whether the cardholder still has to prove they are present before the agent can reach their account. Present on every response, true or false, so a client reads a field rather than an absence. When true, `destinationMasked` and `alreadySent` are present, and the cardholder completes verification by sending the code as an ordinary message. A verification that is outstanding is never reported as a `401`: on this surface a `401` means the platform credential is wrong, or a chat whose verification is still current was driven without its session token. Read this field rather than `verificationStatus`. A chat whose verification has aged past the session lifetime reports `verificationStatus: VERIFIED` and `verificationRequired: true` at the same time, and that combination is the truthful one: the column records that a code was once answered, and this field records whether the agent can act on the account now. - It describes the chat as of the moment the turn started, so the one turn in which the cardholder read their code back to the agent 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. + 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. example: true id: type: string @@ -28038,7 +27796,7 @@ components: example: My card [redacted card number ending 1111] was declined reply: type: string - description: The agent's answer to this message, to render as the assistant's turn. Present only when a turn ran. When verification is outstanding the agent's own sentence asks for the code, so render it beside your code input rather than instead of it. + description: The agent's answer to this message, to render as the assistant's turn. Present only when a turn ran. 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. example: I can help with that. To look at your account I've texted you a code — what does it say? replayed: type: boolean @@ -28061,11 +27819,11 @@ components: sessionToken: type: string description: |- - The session token for this chat, present only on the turn in which the cardholder verified themselves inside the conversation, and absent on every other turn. + The session token for this chat, present only on the turn in which the cardholder verified themselves, and absent on every other turn. - A cardholder can read their code back as a message rather than into your code input. The agent puts it to the same provider, through the same check `POST /care/chats/{chatId}/verifications/confirm` performs, and the chat comes out of that turn able to reach the account. This field is how the partner is handed the credential that turn earned. Without it you would hold a chat that is privileged and unusable at once, because every later message on it is refused with `CARE_SESSION_EXPIRED`. + The cardholder completes verification by sending the SMS code as an ordinary message. The agent checks it with the SMS provider, and the chat comes out of that turn able to reach the account. This field is how the partner is handed the credential that turn earned, and it is the only place a session token is ever issued. Without it you would hold a chat that is privileged and unusable at once, because every later message on it is refused with `CARE_SESSION_EXPIRED`. - It is the same credential, under the same field name and with the same meaning, as the one `POST /care/chats/{chatId}/verifications/confirm` returns, so one handler in your app can serve both doors. Send it as the `X-Ls-Care-Session` header from the next call onwards, and treat it the way you treat the confirm route's: returned once, never re-readable, never logged. + Send it as the `X-Ls-Care-Session` header from the next call onwards. It is returned once and never re-readable, there is no revocation, 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. example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl @@ -28099,11 +27857,10 @@ components: | Error Code | HTTP | Description | |------------|------|-------------| - | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `customerId` that is not a `Customer` id, a code of the wrong shape, or a chat that cannot be verified at all. | + | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `customerId` that is not a `Customer` id, or a chat that cannot be verified at all. | | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused at the content boundary and no turn ran. `reason` carries the sentence to show them. | - | `CARE_VERIFICATION_FAILED` | 400 | The code did not match, or the chat's existing verification is older than the session lifetime and so cannot mint another session. Send a new code and confirm it, or send the next message and let the agent ask for one. | | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified — that is signalled in a `200` body. | - | `CARE_SESSION_EXPIRED` | 401 | The chat's verification is still current, so it requires `X-Ls-Care-Session`, and the header was missing; or the token presented was malformed or past its expiry. Send the cardholder a new code and confirm it. Once the chat's own verification has aged past the session lifetime this error stops: the header is no longer required, and the next message sends a fresh code instead of being refused. | + | `CARE_SESSION_EXPIRED` | 401 | The chat's verification is still current, so it requires `X-Ls-Care-Session`, and the header was missing; or the token presented was malformed or past its expiry. Retry the message with no `X-Ls-Care-Session` header. If that is refused too, the token for a live verification has been lost: close the chat and send the message to the cardholder as `Customer:`, which opens a fresh chat and sends a new code. Once the chat's own verification has aged past the session lifetime this error stops: the header is no longer required, and the next message sends a fresh code instead of being refused. | | `FORBIDDEN` | 403 | Embedded care is not enabled for this platform. | | `CARE_SESSION_PLATFORM_MISMATCH` | 403 | The session token was minted for a different platform. Stop; retrying will not help. | | `NOT_FOUND` | 404 | No such chat for this platform. A chat belonging to another platform, a chat that never existed, a malformed chat id, and an `X-Ls-Care-Session` token minted for a different chat or a different cardholder are one answer. | @@ -28121,7 +27878,6 @@ components: enum: - INVALID_INPUT - CARE_MESSAGE_REJECTED - - CARE_VERIFICATION_FAILED - UNAUTHORIZED - CARE_SESSION_EXPIRED - FORBIDDEN @@ -28143,85 +27899,6 @@ components: type: string description: Human-readable explanation, for a log or a support ticket. Not for branching, and not always safe to show a cardholder verbatim — the exception is `CARE_MESSAGE_REJECTED`, whose `reason` is written to be shown. example: The care session has expired. Verify the cardholder again. - CareChatVerificationRequest: - type: object - description: 'Options for re-sending the cardholder''s verification code. The body may be omitted entirely, which is the same as `{"resend": false}`.' - properties: - resend: - type: boolean - default: false - description: |- - `false` asks for a code and is content to be told one is already outstanding: the response then carries `alreadySent: true` and no new SMS is sent. That is a success, not an error — the cardholder should read back the code they already have. - - `true` is the deliberate second code, for a cardholder who says the first never arrived. Ask before you send one: codes are sent from a pool of numbers, so a second code arrives from a different sender than the first and leaves the cardholder holding two codes from two strangers, deciding which to read back. - example: false - CareChatVerificationResponse: - type: object - description: 'Acknowledgement that a verification code is on its way to the cardholder. `202`, not `200`: the SMS is in flight, not delivered.' - required: - - status - - destinationMasked - - alreadySent - properties: - status: - type: string - enum: - - SENT - description: Always `SENT`. The chat's own state is reported as `verificationStatus` on the message route. - example: SENT - destinationMasked: - type: string - description: The masked phone number the code is going to, for a UI to show as "we texted ...". Always the number already on file for the cardholder; the caller cannot nominate a destination. - example: '***0001' - alreadySent: - type: boolean - description: 'True when a code was already outstanding and this call sent no new SMS. Show "we already texted you" rather than implying a fresh send. A request with `resend: true` sends a new code and returns false.' - example: true - CareChatVerificationConfirmRequest: - type: object - description: The code the cardholder read back from their SMS. - required: - - code - properties: - code: - type: string - minLength: 1 - maxLength: 32 - description: The numeric code from the SMS, exactly as the cardholder entered it. A code of the wrong shape is rejected with `INVALID_INPUT`; a well-formed code that the provider says is not the one it sent is rejected with `CARE_VERIFICATION_FAILED`, which is the error to retry by asking the cardholder again. - example: '482913' - CareChatSession: - type: object - description: Proof that the cardholder answered their code, as a signed token the partner sends back on every later message. The message route returns the same two fields, under the same names, on the turn in which the cardholder read their code back to the agent instead of into a code input. - required: - - status - - sessionToken - - expiresAt - properties: - status: - type: string - enum: - - VERIFIED - description: Always `VERIFIED`; a code that did not match is an error, not a status. - example: VERIFIED - sessionToken: - type: string - description: |- - The value to send as the `X-Ls-Care-Session` header on every later message on this chat. - - **It is returned here and never again.** There is no route that reads it back, and there is no revocation: the short expiry is what makes that acceptable. Hold it in the cardholder's session on your side for as long as they are chatting, and treat losing it as re-verification — send a new code with `POST /care/chats/{chatId}/verifications` and confirm it again. 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. A later message on a chat that is already verified returns no new token, and the one you hold stays the right one until `expiresAt`. - - The token is bound to this platform, this chat and this cardholder, so it cannot drive another chat and does not survive being handed to another platform. - example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl - expiresAt: - type: string - format: date-time - description: |- - ISO 8601 timestamp, in UTC, after which the token is refused with `CARE_SESSION_EXPIRED`. Sessions are short-lived by design — fifteen minutes in the default deployment. Read this field rather than assuming a duration. - - The same window governs the chat itself: a verification older than the session lifetime no longer reaches the cardholder's account, so a chat whose token has expired is not a chat waiting for a token. It is a chat that needs a new code, and the next message on it will send one and have the agent ask for it. - example: '2026-09-12T14:25:00Z' WebhookType: type: string enum: diff --git a/openapi.yaml b/openapi.yaml index 18a918e34..1d21fec17 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -67,37 +67,37 @@ tags: from the partner's own app. The platform calls with its ordinary Grid credential, but the person typing is the cardholder rather than the caller, so reaching that cardholder's account takes a second proof: an - SMS code they answer, which the partner afterwards carries in the - `X-Ls-Care-Session` header. + SMS code they send into the chat, after which the partner carries a + session token in the `X-Ls-Care-Session` header. - **The surface is four routes, and the happy path is two calls.** + **The surface is two routes, and verification happens inside the + conversation.** 1. **Open the chat.** `POST /care/chats/{chatId}/messages` with an empty `message`, addressed to the cardholder as `Customer:`. Returns the chat's id and the agent's `greeting`, which is what a chat window renders before anybody has typed. Sends no SMS, runs no turn, bills - nothing, and is safe to call again — a widget that reloaded gets the + nothing, and is safe to call again: a widget that reloaded gets the same chat back, this time without a greeting. 2. **Send the cardholder's first message.** The same route with their text and an `Idempotency-Key` header. One turn runs synchronously and the agent's answer comes back in `reply`. On a chat that cannot yet reach the account this call also texts the verification code and says so in the same body: `verificationRequired: true`, with - `destinationMasked`, `alreadySent` and `codeLength` for the input you - put on screen. - 3. **Confirm the code.** - `POST /care/chats/{chatId}/verifications/confirm` returns a - `sessionToken` and its `expiresAt`. Send that token as the - `X-Ls-Care-Session` header on every message from then on. A - cardholder who reads the code back to the agent as an ordinary - message instead is confirmed the same way, and that turn's own - response carries the same two fields. + `destinationMasked`, `alreadySent` and `codeLength`, and the agent's + `reply` asks the cardholder for the code. + 3. **The cardholder sends the code as an ordinary message.** The agent + checks it with the SMS provider, and the response to that turn + carries `sessionToken` and `expiresAt`. Send that token as the + `X-Ls-Care-Session` header on every message from then on. A code that + does not match is answered in `reply`, and the cardholder can simply + send it again. 4. **Close the chat** with `DELETE /care/chats/{chatId}` when the cardholder is done. - `POST /care/chats/{chatId}/verifications` is the *Send another code* - button rather than a step in the flow: step 2 already sent the first - code. + **A cardholder whose code never arrived asks the agent for another + one.** The agent sends a new code to the number on file and says so in + its `reply`. There is no separate route for it. Step 1 is optional. A message addressed to a `Customer:` id opens that cardholder's chat if they have none, so a partner whose cardholder has @@ -115,13 +115,16 @@ tags: than the session lifetime, fifteen minutes in the default deployment, which is also how long a session token lives. Past that window the chat stops requiring the header: a message sent without one gets a fresh - code, and the agent asks the cardholder to read it back inside the + code, and the agent asks the cardholder to send it back inside the conversation, which hands the partner the next session token. Sending the dead token anyway is still a `401 CARE_SESSION_EXPIRED`, so the handling is to discard the token on that error and resend the message with no `X-Ls-Care-Session` header rather than to tear the chat down. If that resend is refused too, the chat's verification is still current - and the way back is a new code. + and its token has been lost. Close the chat with + `DELETE /care/chats/{chatId}`, which needs no token, and send the + message to the cardholder as `Customer:`: that opens a fresh chat + and sends a new code. Two things an integration has to get right: @@ -132,10 +135,9 @@ tags: and into every log the transcript reaches. - **The session token is returned once and is never re-readable.** No endpoint hands it back, there is no revocation, and it is short-lived - by design. Hold it for the life of the cardholder's session; a partner - that drops it has to send a new code and confirm it again. It arrives - from whichever door took the code, so read `sessionToken` on the - message response as well as on the confirm response. + by design. Hold it for the life of the cardholder's session. It + arrives on the message response to the turn in which the cardholder + sent their code, and on no other. Some things are absent by construction, so no integration can come to depend on them: nothing returns a transcript or an earlier turn's reply @@ -10693,8 +10695,6 @@ paths: `codeLength` come back with it, and the agent's own sentence in `reply` asks the cardholder for the code. Later messages on the same chat find the code already outstanding and send nothing, reporting `alreadySent: true`. - Collect the code and post it to - `POST /care/chats/{chatId}/verifications/confirm`. **Render `displayMessage`, not your own copy of the input.** Grid redacts the cardholder's text on the way in — a card number they pasted comes back @@ -10702,26 +10702,23 @@ paths: own input box instead has put an unmasked PAN in the transcript, on the screen and in whatever logs the transcript reaches. - **The code can also be read back to the agent, and then this route issues - the session.** When the cardholder types their code as a message rather - than into your code input, the agent puts it to the same provider, through - the same check `POST /care/chats/{chatId}/verifications/confirm` performs, - and the chat comes out of that turn able to reach the account. That turn's - body carries `sessionToken` and `expiresAt`, under the same names and with - the same meaning as the confirm route's, so one handler in your app serves - both doors. 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, or you will hold a chat that is privileged and unusable at - once. - - The structured route is not deprecated by this, and it is still the one to - call when your own UI collects the code: only it can tell you where the - code went, whether one was already in flight, and how many digits it has. + **The cardholder completes verification by sending the code as an + ordinary message, and that turn's response carries the session.** The + agent checks the code with the SMS provider, and the chat comes out of + that turn able to reach the account. 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, or you will hold a chat that is privileged and + unusable at once. 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, every message must carry the - session token in the `X-Ls-Care-Session` header, whichever of the two - doors issued it. Without it the call is refused with - `CARE_SESSION_EXPIRED`. + session token in the `X-Ls-Care-Session` header. Without it the call is + refused with `CARE_SESSION_EXPIRED`. **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 @@ -10732,18 +10729,20 @@ paths: being required, and the next message **sent without it** is treated as an unverified one: a fresh code goes out, the body carries `verificationRequired: true` with `destinationMasked`, `alreadySent` and - `codeLength`, and the agent's own `reply` asks the cardholder to read the - code back. The turn still runs and still answers. + `codeLength`, and the agent's own `reply` asks the cardholder to send the + code. The turn still runs and still answers. Sending the expired token anyway is still a `401 CARE_SESSION_EXPIRED`. One recovery covers a token that was lost, one that was never valid and one that has expired: retry the message with no `X-Ls-Care-Session` header. On a chat whose verification has gone stale that call succeeds, sends a fresh code and has the agent ask for it, and the turn in which the - cardholder reads it back returns the next `sessionToken`. On a chat whose + cardholder sends it returns the next `sessionToken`. On a chat whose verification is still current it is refused with `CARE_SESSION_EXPIRED` - again, which is the signal to send a code with - `POST /care/chats/{chatId}/verifications` and confirm it. An empty message + again, which means the token for a live verification has been lost: close + the chat with `DELETE /care/chats/{chatId}`, which needs no token, and + send the message to the cardholder as `Customer:`, which opens a + fresh chat and sends a new code. 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. @@ -10772,7 +10771,7 @@ paths: - name: X-Ls-Care-Session in: header required: false - description: The `sessionToken` from `POST /care/chats/{chatId}/verifications/confirm`, or the one this route returned on the turn that verified the cardholder in the conversation. Required on every message while the chat's verification is current, and refused with `CARE_SESSION_EXPIRED` when it is missing there. Not required before the cardholder has verified, nor once the chat's verification has aged past the session lifetime; but a token that is sent is always checked, so a malformed or expired one is refused with `CARE_SESSION_EXPIRED` whatever state the chat is in, and one minted for another chat or another cardholder is `NOT_FOUND`. An opening never reads the header at all, because a widget that reloaded has lost its token and that is the case the opening exists for. + description: The `sessionToken` this route returned on the turn in which the cardholder sent their verification code. Required on every message while the chat's verification is current, and refused with `CARE_SESSION_EXPIRED` when it is missing there. Not required before the cardholder has verified, nor once the chat's verification has aged past the session lifetime; but a token that is sent is always checked, so a malformed or expired one is refused with `CARE_SESSION_EXPIRED` whatever state the chat is in, and one minted for another chat or another cardholder is `NOT_FOUND`. An opening never reads the header at all, because a widget that reloaded has lost its token and that is the case the opening exists for. schema: type: string example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl @@ -10791,6 +10790,10 @@ paths: summary: The cardholder's first message, which also sends the code value: message: My card was declined at the grocery store + verificationCode: + summary: The cardholder sends their code, and this turn returns the session + value: + message: The code is 482913 afterVerification: summary: A later message, sent with the X-Ls-Care-Session header value: @@ -10868,251 +10871,6 @@ paths: application/json: schema: $ref: '#/components/schemas/CareError' - /care/chats/{chatId}/verifications: - parameters: - - name: chatId - in: path - description: The chat to send another code on, as returned in `chatId` by `POST /care/chats/{chatId}/messages`. - required: true - schema: - type: string - example: CareChat:019542f5-b3e7-1d02-0000-000000000030 - post: - summary: Resend a care chat verification code - description: | - Send the cardholder another verification code. - - **This is the resend, not the first send.** The cardholder's first message - already sent a code and said so in its own body; this is what the *Send - another code* button calls when they say it never arrived. A partner never - has to call it to start verification, and does not have to call it to - recover a chat whose verification has gone stale either, because the next - message sends a code by itself. - - With `resend` false — the default, and the same as sending no body — the - call asks for a code and is content to be told one is already outstanding: - the answer carries `alreadySent: true` and no new SMS goes out. That is a - success. With `resend` true a second code is sent. Ask the cardholder - before doing that: codes are sent from a pool of numbers, so the second - arrives from a different sender than the first, leaving them holding two - codes from two strangers and deciding which to read back. - - The destination is always the number already on file for the cardholder. - There is no way to nominate one, because a code sent to a number the - caller chose would prove nothing about the person holding the card. - - `202`, not `200`: the SMS is in flight, not delivered. - - A chat that is already verified is refused with `INVALID_INPUT` while its - verification is recent — there is nothing left to prove. Once that - verification has aged past the session lifetime the chat no longer reaches - the cardholder's account, so there is something to prove again: the - refusal lifts on its own and this route sends a fresh code, whatever - `verificationStatus` still says. - operationId: resendCareChatVerification - tags: - - Embedded Care - security: - - BasicAuth: [] - requestBody: - required: false - content: - application/json: - schema: - $ref: '#/components/schemas/CareChatVerificationRequest' - examples: - askForACode: - summary: Ask for a code, accepting the one already outstanding - value: - resend: false - deliberateResend: - summary: The cardholder says the first code never arrived - value: - resend: true - responses: - '202': - description: The code is on its way, or one was already outstanding and none was sent — `alreadySent` says which. - content: - application/json: - schema: - $ref: '#/components/schemas/CareChatVerificationResponse' - '400': - description: '`INVALID_INPUT` when the chat is already verified, when the body carries an unknown field, or when verification is not available for this chat at all — the platform''s care programme does not offer SMS verification, or the cardholder record behind the chat could not be read.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '401': - description: '`UNAUTHORIZED` — the platform''s Grid credential is missing or wrong.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '403': - description: '`FORBIDDEN` — embedded care is not enabled for this platform.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '404': - description: '`NOT_FOUND` — no such chat for this platform. A chat that belongs to another platform, one that never existed, and a malformed chat id are one answer.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '409': - description: '`CARE_CHAT_ENDED` when the chat has been closed, or `CONFLICT` when it has no cardholder bound to it.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '429': - description: '`CARE_BUDGET_EXCEEDED` when this chat has reached its limit, or `RATE_LIMITED` when the platform is calling this endpoint too often; back off for the interval in the `Retry-After` header.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '500': - description: Internal service error. - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '502': - description: '`CARE_VERIFICATION_UNAVAILABLE` — the SMS provider could not be reached. The factor is down, so retry rather than asking the cardholder for anything.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '503': - description: '`SERVICE_UNAVAILABLE` when embedded care is not available in this deployment, or the support agent is temporarily turned off.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - /care/chats/{chatId}/verifications/confirm: - parameters: - - name: chatId - in: path - description: The chat the code was sent on, as returned in `chatId` by `POST /care/chats/{chatId}/messages`. - required: true - schema: - type: string - example: CareChat:019542f5-b3e7-1d02-0000-000000000030 - post: - summary: Confirm a care chat verification code - description: | - Check the code the cardholder read back, and hand over the session that - every later message on this chat rides on. - - **The session token is returned here and never again.** No endpoint reads - it back, and there is no revocation — the short expiry is what makes that - acceptable. Keep it in the cardholder's own session on your side, send it - as the `X-Ls-Care-Session` header on every later message, and treat losing - it as re-verification: send a new code with - `POST /care/chats/{chatId}/verifications` and confirm it again. Read - `expiresAt` rather than assuming a lifetime. - - **This is not the only door.** A cardholder can read the code back to the - agent as an ordinary message, and `POST /care/chats/{chatId}/messages` - then returns the same `sessionToken` and `expiresAt` on that turn. Call - this route when your own UI collects the code, because only it can tell - you where the code went, whether one was already in flight, and how many - digits it has. - - The token is bound to this platform, this chat and this cardholder. It - cannot drive another chat, and presenting it to another platform's chat is - refused with `CARE_SESSION_PLATFORM_MISMATCH`. - - A code that does not match is `CARE_VERIFICATION_FAILED` — ask the - cardholder again. A provider that could not be reached is - `CARE_VERIFICATION_UNAVAILABLE` — retry; nothing the cardholder does will - help. The two are kept apart deliberately, because they call for opposite - reactions. - - A chat whose verification is older than the session lifetime will not mint - a fresh session off it: that would be the platform issuing itself a - session for a cardholder who has not shown themselves in longer than a - session lasts. It is refused with `CARE_VERIFICATION_FAILED` before the - provider is reached, and the way forward is a new code. That is the same - window that decides whether the chat reaches the cardholder's account at - all, so a stale chat is not a locked one: sending a code with - `POST /care/chats/{chatId}/verifications`, or simply sending the next - message and letting the agent ask, both open it again. - operationId: confirmCareChatVerification - tags: - - Embedded Care - security: - - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CareChatVerificationConfirmRequest' - example: - code: '482913' - responses: - '200': - description: The cardholder is verified on this chat, and the session token is in this body — it is not retrievable afterwards. - content: - application/json: - schema: - $ref: '#/components/schemas/CareChatSession' - '400': - description: '`CARE_VERIFICATION_FAILED` when the code did not match, or when the chat''s existing verification is too old to mint another session. `INVALID_INPUT` when the code is not of the shape the SMS carries, when the body is missing or carries an unknown field, or when verification is not available for this chat.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '401': - description: '`UNAUTHORIZED` — the platform''s Grid credential is missing or wrong.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '403': - description: '`FORBIDDEN` — embedded care is not enabled for this platform.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '404': - description: '`NOT_FOUND` — no such chat for this platform. A chat that belongs to another platform, one that never existed, and a malformed chat id are one answer.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '409': - description: '`CARE_CHAT_ENDED` when the chat has been closed, or `CONFLICT` when it has no cardholder bound to it.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '429': - description: '`CARE_BUDGET_EXCEEDED` when this chat has reached its limit, or `RATE_LIMITED` when the platform is calling this endpoint too often; back off for the interval in the `Retry-After` header.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '500': - description: Internal service error. - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '502': - description: '`CARE_VERIFICATION_UNAVAILABLE` — the SMS provider could not be reached, so the code could not be checked. Retry; the cardholder has done nothing wrong.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' - '503': - description: '`SERVICE_UNAVAILABLE` when embedded care is not available in this deployment, or the support agent is temporarily turned off.' - content: - application/json: - schema: - $ref: '#/components/schemas/CareError' /care/chats/{chatId}: parameters: - name: chatId @@ -27981,9 +27739,9 @@ components: |--------|-------------| | `PENDING` | No code has been sent on this chat yet. This is where every new chat starts. | | `SENT` | A code has been sent to the cardholder and is outstanding. | - | `VERIFIED` | The cardholder answered the code, either at `POST /care/chats/{chatId}/verifications/confirm` or by reading it back to the agent. Account-reaching tools are available for as long as that answer is newer than the session lifetime, and a session token was issued by whichever of the two doors took the code. | + | `VERIFIED` | The cardholder sent the code as a message and the agent confirmed it. Account-reaching tools are available for as long as that answer is newer than the session lifetime, and the session token was issued on the message response to that turn. | | `TRUSTED` | The cardholder is treated as present without an SMS code on this chat. Reserved for channels Grid can already attribute; a partner chat does not start here. Bounded by the same freshness window as `VERIFIED`. | - | `FAILED` | The last code was answered incorrectly or expired. Send another code with `POST /care/chats/{chatId}/verifications`. | + | `FAILED` | The last code was answered incorrectly or expired. The cardholder can send the code again, or ask the agent for a new one. | CareChatGreeting: type: object description: 'The agent''s opening line, recorded as the chat''s first message. Returned only by the call that opened the chat, never on a resume: a greeting arriving mid-conversation reads as the agent introducing itself a second time. Absent when the agent produced no opening line.' @@ -28007,7 +27765,7 @@ components: example: '2026-09-12T14:10:00.123456Z' CareChatMessageResponse: type: object - description: 'The result of one call to the message route. Which fields are present depends on what the call did: a turn adds `id`, `displayMessage`, `reply` and `replayed`; the call that opened the chat adds `greeting`; an outstanding verification adds `destinationMasked`, `alreadySent` and, where the service could be read, `codeLength`; and a turn in which the cardholder read their code back to the agent adds `sessionToken` and `expiresAt`.' + description: 'The result of one call to the message route. Which fields are present depends on what the call did: a turn adds `id`, `displayMessage`, `reply` and `replayed`; the call that opened the chat adds `greeting`; an outstanding verification adds `destinationMasked`, `alreadySent` and, where the service could be read, `codeLength`; and the turn in which the cardholder sent their verification code adds `sessionToken` and `expiresAt`.' required: - chatId - verificationStatus @@ -28015,18 +27773,18 @@ components: properties: chatId: type: string - description: 'The chat this call ran on. When the request addressed a cardholder (`Customer:` in the path) this is the chat that was found or opened for them. Use it for every later call on this conversation: confirming the code, resending it, and closing the chat.' + description: 'The chat this call ran on. When the request addressed a cardholder (`Customer:` 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.' example: CareChat:019542f5-b3e7-1d02-0000-000000000030 verificationStatus: $ref: '#/components/schemas/CareChatVerificationStatus' verificationRequired: type: boolean description: |- - Whether the cardholder still has to prove they are present before the agent can reach their account. Present on every response, true or false, so a client reads a field rather than an absence. When true, `destinationMasked` and `alreadySent` are present and your UI should collect the code and post it to `POST /care/chats/{chatId}/verifications/confirm`. A verification that is outstanding is never reported as a `401`: on this surface a `401` means the platform credential is wrong, or a chat whose verification is still current was driven without its session token. + Whether the cardholder still has to prove they are present before the agent can reach their account. Present on every response, true or false, so a client reads a field rather than an absence. When true, `destinationMasked` and `alreadySent` are present, and the cardholder completes verification by sending the code as an ordinary message. A verification that is outstanding is never reported as a `401`: on this surface a `401` means the platform credential is wrong, or a chat whose verification is still current was driven without its session token. Read this field rather than `verificationStatus`. A chat whose verification has aged past the session lifetime reports `verificationStatus: VERIFIED` and `verificationRequired: true` at the same time, and that combination is the truthful one: the column records that a code was once answered, and this field records whether the agent can act on the account now. - It describes the chat as of the moment the turn started, so the one turn in which the cardholder read their code back to the agent 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. + 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. example: true id: type: string @@ -28038,7 +27796,7 @@ components: example: My card [redacted card number ending 1111] was declined reply: type: string - description: The agent's answer to this message, to render as the assistant's turn. Present only when a turn ran. When verification is outstanding the agent's own sentence asks for the code, so render it beside your code input rather than instead of it. + description: The agent's answer to this message, to render as the assistant's turn. Present only when a turn ran. 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. example: I can help with that. To look at your account I've texted you a code — what does it say? replayed: type: boolean @@ -28061,11 +27819,11 @@ components: sessionToken: type: string description: |- - The session token for this chat, present only on the turn in which the cardholder verified themselves inside the conversation, and absent on every other turn. + The session token for this chat, present only on the turn in which the cardholder verified themselves, and absent on every other turn. - A cardholder can read their code back as a message rather than into your code input. The agent puts it to the same provider, through the same check `POST /care/chats/{chatId}/verifications/confirm` performs, and the chat comes out of that turn able to reach the account. This field is how the partner is handed the credential that turn earned. Without it you would hold a chat that is privileged and unusable at once, because every later message on it is refused with `CARE_SESSION_EXPIRED`. + The cardholder completes verification by sending the SMS code as an ordinary message. The agent checks it with the SMS provider, and the chat comes out of that turn able to reach the account. This field is how the partner is handed the credential that turn earned, and it is the only place a session token is ever issued. Without it you would hold a chat that is privileged and unusable at once, because every later message on it is refused with `CARE_SESSION_EXPIRED`. - It is the same credential, under the same field name and with the same meaning, as the one `POST /care/chats/{chatId}/verifications/confirm` returns, so one handler in your app can serve both doors. Send it as the `X-Ls-Care-Session` header from the next call onwards, and treat it the way you treat the confirm route's: returned once, never re-readable, never logged. + Send it as the `X-Ls-Care-Session` header from the next call onwards. It is returned once and never re-readable, there is no revocation, 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. example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl @@ -28099,11 +27857,10 @@ components: | Error Code | HTTP | Description | |------------|------|-------------| - | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `customerId` that is not a `Customer` id, a code of the wrong shape, or a chat that cannot be verified at all. | + | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `customerId` that is not a `Customer` id, or a chat that cannot be verified at all. | | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused at the content boundary and no turn ran. `reason` carries the sentence to show them. | - | `CARE_VERIFICATION_FAILED` | 400 | The code did not match, or the chat's existing verification is older than the session lifetime and so cannot mint another session. Send a new code and confirm it, or send the next message and let the agent ask for one. | | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified — that is signalled in a `200` body. | - | `CARE_SESSION_EXPIRED` | 401 | The chat's verification is still current, so it requires `X-Ls-Care-Session`, and the header was missing; or the token presented was malformed or past its expiry. Send the cardholder a new code and confirm it. Once the chat's own verification has aged past the session lifetime this error stops: the header is no longer required, and the next message sends a fresh code instead of being refused. | + | `CARE_SESSION_EXPIRED` | 401 | The chat's verification is still current, so it requires `X-Ls-Care-Session`, and the header was missing; or the token presented was malformed or past its expiry. Retry the message with no `X-Ls-Care-Session` header. If that is refused too, the token for a live verification has been lost: close the chat and send the message to the cardholder as `Customer:`, which opens a fresh chat and sends a new code. Once the chat's own verification has aged past the session lifetime this error stops: the header is no longer required, and the next message sends a fresh code instead of being refused. | | `FORBIDDEN` | 403 | Embedded care is not enabled for this platform. | | `CARE_SESSION_PLATFORM_MISMATCH` | 403 | The session token was minted for a different platform. Stop; retrying will not help. | | `NOT_FOUND` | 404 | No such chat for this platform. A chat belonging to another platform, a chat that never existed, a malformed chat id, and an `X-Ls-Care-Session` token minted for a different chat or a different cardholder are one answer. | @@ -28121,7 +27878,6 @@ components: enum: - INVALID_INPUT - CARE_MESSAGE_REJECTED - - CARE_VERIFICATION_FAILED - UNAUTHORIZED - CARE_SESSION_EXPIRED - FORBIDDEN @@ -28143,85 +27899,6 @@ components: type: string description: Human-readable explanation, for a log or a support ticket. Not for branching, and not always safe to show a cardholder verbatim — the exception is `CARE_MESSAGE_REJECTED`, whose `reason` is written to be shown. example: The care session has expired. Verify the cardholder again. - CareChatVerificationRequest: - type: object - description: 'Options for re-sending the cardholder''s verification code. The body may be omitted entirely, which is the same as `{"resend": false}`.' - properties: - resend: - type: boolean - default: false - description: |- - `false` asks for a code and is content to be told one is already outstanding: the response then carries `alreadySent: true` and no new SMS is sent. That is a success, not an error — the cardholder should read back the code they already have. - - `true` is the deliberate second code, for a cardholder who says the first never arrived. Ask before you send one: codes are sent from a pool of numbers, so a second code arrives from a different sender than the first and leaves the cardholder holding two codes from two strangers, deciding which to read back. - example: false - CareChatVerificationResponse: - type: object - description: 'Acknowledgement that a verification code is on its way to the cardholder. `202`, not `200`: the SMS is in flight, not delivered.' - required: - - status - - destinationMasked - - alreadySent - properties: - status: - type: string - enum: - - SENT - description: Always `SENT`. The chat's own state is reported as `verificationStatus` on the message route. - example: SENT - destinationMasked: - type: string - description: The masked phone number the code is going to, for a UI to show as "we texted ...". Always the number already on file for the cardholder; the caller cannot nominate a destination. - example: '***0001' - alreadySent: - type: boolean - description: 'True when a code was already outstanding and this call sent no new SMS. Show "we already texted you" rather than implying a fresh send. A request with `resend: true` sends a new code and returns false.' - example: true - CareChatVerificationConfirmRequest: - type: object - description: The code the cardholder read back from their SMS. - required: - - code - properties: - code: - type: string - minLength: 1 - maxLength: 32 - description: The numeric code from the SMS, exactly as the cardholder entered it. A code of the wrong shape is rejected with `INVALID_INPUT`; a well-formed code that the provider says is not the one it sent is rejected with `CARE_VERIFICATION_FAILED`, which is the error to retry by asking the cardholder again. - example: '482913' - CareChatSession: - type: object - description: Proof that the cardholder answered their code, as a signed token the partner sends back on every later message. The message route returns the same two fields, under the same names, on the turn in which the cardholder read their code back to the agent instead of into a code input. - required: - - status - - sessionToken - - expiresAt - properties: - status: - type: string - enum: - - VERIFIED - description: Always `VERIFIED`; a code that did not match is an error, not a status. - example: VERIFIED - sessionToken: - type: string - description: |- - The value to send as the `X-Ls-Care-Session` header on every later message on this chat. - - **It is returned here and never again.** There is no route that reads it back, and there is no revocation: the short expiry is what makes that acceptable. Hold it in the cardholder's session on your side for as long as they are chatting, and treat losing it as re-verification — send a new code with `POST /care/chats/{chatId}/verifications` and confirm it again. 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. A later message on a chat that is already verified returns no new token, and the one you hold stays the right one until `expiresAt`. - - The token is bound to this platform, this chat and this cardholder, so it cannot drive another chat and does not survive being handed to another platform. - example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl - expiresAt: - type: string - format: date-time - description: |- - ISO 8601 timestamp, in UTC, after which the token is refused with `CARE_SESSION_EXPIRED`. Sessions are short-lived by design — fifteen minutes in the default deployment. Read this field rather than assuming a duration. - - The same window governs the chat itself: a verification older than the session lifetime no longer reaches the cardholder's account, so a chat whose token has expired is not a chat waiting for a token. It is a chat that needs a new code, and the next message on it will send one and have the agent ask for it. - example: '2026-09-12T14:25:00Z' WebhookType: type: string enum: diff --git a/openapi/components/schemas/care/CareChatMessageResponse.yaml b/openapi/components/schemas/care/CareChatMessageResponse.yaml index b7df00b07..7ce98181e 100644 --- a/openapi/components/schemas/care/CareChatMessageResponse.yaml +++ b/openapi/components/schemas/care/CareChatMessageResponse.yaml @@ -4,8 +4,8 @@ description: >- depends on what the call did: a turn adds `id`, `displayMessage`, `reply` and `replayed`; the call that opened the chat adds `greeting`; an outstanding verification adds `destinationMasked`, `alreadySent` and, - where the service could be read, `codeLength`; and a turn in which the - cardholder read their code back to the agent adds `sessionToken` and + where the service could be read, `codeLength`; and the turn in which the + cardholder sent their verification code adds `sessionToken` and `expiresAt`. required: - chatId @@ -18,7 +18,7 @@ properties: The chat this call ran on. When the request addressed a cardholder (`Customer:` in the path) this is the chat that was found or opened for them. Use it for every later call on this conversation: - confirming the code, resending it, and closing the chat. + later messages and closing the chat. example: CareChat:019542f5-b3e7-1d02-0000-000000000030 verificationStatus: $ref: ./CareChatVerificationStatus.yaml @@ -28,9 +28,9 @@ properties: Whether the cardholder still has to prove they are present before the agent can reach their account. Present on every response, true or false, so a client reads a field rather than an absence. When true, - `destinationMasked` and `alreadySent` are present and your UI should - collect the code and post it to - `POST /care/chats/{chatId}/verifications/confirm`. A verification that + `destinationMasked` and `alreadySent` are present, and the cardholder + completes verification by sending the code as an ordinary message. A + verification that is outstanding is never reported as a `401`: on this surface a `401` means the platform credential is wrong, or a chat whose verification is still current was driven without its session token. @@ -45,7 +45,7 @@ properties: It describes the chat as of the moment the turn started, so the one turn - in which the cardholder read their code back to the agent reports + 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. @@ -70,8 +70,8 @@ properties: description: >- The agent's answer to this message, to render as the assistant's turn. Present only when a turn ran. When verification is outstanding the - agent's own sentence asks for the code, so render it beside your code - input rather than instead of it. + 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. example: I can help with that. To look at your account I've texted you a code — what does it say? replayed: type: boolean @@ -113,26 +113,22 @@ properties: type: string description: >- The session token for this chat, present only on the turn in which the - cardholder verified themselves inside the conversation, and absent on - every other turn. + cardholder verified themselves, and absent on every other turn. - A cardholder can read their code back as a message rather than into - your code input. The agent puts it to the same provider, through the - same check `POST /care/chats/{chatId}/verifications/confirm` performs, - and the chat comes out of that turn able to reach the account. - This field is how the partner is handed the credential that turn earned. - Without it you would hold a chat that is privileged and unusable at - once, because every later message on it is refused with - `CARE_SESSION_EXPIRED`. + The cardholder completes verification by sending the SMS code as an + ordinary message. The agent checks it with the SMS provider, and the + chat comes out of that turn able to reach the account. This field is + how the partner is handed the credential that turn earned, and it is the + only place a session token is ever issued. Without it you would hold a + chat that is privileged and unusable at once, because every later + message on it is refused with `CARE_SESSION_EXPIRED`. - It is the same credential, under the same field name and with the same - meaning, as the one `POST /care/chats/{chatId}/verifications/confirm` - returns, so one handler in your app can serve both doors. Send it as the - `X-Ls-Care-Session` header from the next call onwards, and treat it the - way you treat the confirm route's: returned once, never re-readable, - never logged. + Send it as the `X-Ls-Care-Session` header from the next call onwards. + It is returned once and never re-readable, there is no revocation, 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 diff --git a/openapi/components/schemas/care/CareChatSession.yaml b/openapi/components/schemas/care/CareChatSession.yaml deleted file mode 100644 index 2ea707681..000000000 --- a/openapi/components/schemas/care/CareChatSession.yaml +++ /dev/null @@ -1,58 +0,0 @@ -type: object -description: >- - Proof that the cardholder answered their code, as a signed token the - partner sends back on every later message. The message route returns the - same two fields, under the same names, on the turn in which the cardholder - read their code back to the agent instead of into a code input. -required: - - status - - sessionToken - - expiresAt -properties: - status: - type: string - enum: - - VERIFIED - description: Always `VERIFIED`; a code that did not match is an error, not a status. - example: VERIFIED - sessionToken: - type: string - description: >- - The value to send as the `X-Ls-Care-Session` header on every later - message on this chat. - - - **It is returned here and never again.** There is no route that reads - it back, and there is no revocation: the short expiry is what makes - that acceptable. Hold it in the cardholder's session on your side for - as long as they are chatting, and treat losing it as re-verification — - send a new code with `POST /care/chats/{chatId}/verifications` and - confirm it again. 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. A later - message on a chat that is already verified returns no new token, and the - one you hold stays the right one until `expiresAt`. - - - The token is bound to this platform, this chat and this cardholder, so - it cannot drive another chat and does not survive being handed to - another platform. - example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl - expiresAt: - type: string - format: date-time - description: >- - ISO 8601 timestamp, in UTC, after which the token is refused with - `CARE_SESSION_EXPIRED`. Sessions are short-lived by design — fifteen - minutes in the default deployment. Read this field rather than assuming - a duration. - - - The same window governs the chat itself: a verification older than the - session lifetime no longer reaches the cardholder's account, so a chat - whose token has expired is not a chat waiting for a token. It is a chat - that needs a new code, and the next message on it will send one and have - the agent ask for it. - example: '2026-09-12T14:25:00Z' diff --git a/openapi/components/schemas/care/CareChatVerificationConfirmRequest.yaml b/openapi/components/schemas/care/CareChatVerificationConfirmRequest.yaml deleted file mode 100644 index 59a996ee6..000000000 --- a/openapi/components/schemas/care/CareChatVerificationConfirmRequest.yaml +++ /dev/null @@ -1,16 +0,0 @@ -type: object -description: The code the cardholder read back from their SMS. -required: - - code -properties: - code: - type: string - minLength: 1 - maxLength: 32 - description: >- - The numeric code from the SMS, exactly as the cardholder entered it. - A code of the wrong shape is rejected with `INVALID_INPUT`; a - well-formed code that the provider says is not the one it sent is - rejected with `CARE_VERIFICATION_FAILED`, which is the error to retry - by asking the cardholder again. - example: '482913' diff --git a/openapi/components/schemas/care/CareChatVerificationRequest.yaml b/openapi/components/schemas/care/CareChatVerificationRequest.yaml deleted file mode 100644 index dee0da4e7..000000000 --- a/openapi/components/schemas/care/CareChatVerificationRequest.yaml +++ /dev/null @@ -1,21 +0,0 @@ -type: object -description: >- - Options for re-sending the cardholder's verification code. The body may be - omitted entirely, which is the same as `{"resend": false}`. -properties: - resend: - type: boolean - default: false - description: >- - `false` asks for a code and is content to be told one is already - outstanding: the response then carries `alreadySent: true` and no new - SMS is sent. That is a success, not an error — the cardholder should - read back the code they already have. - - - `true` is the deliberate second code, for a cardholder who says the - first never arrived. Ask before you send one: codes are sent from a - pool of numbers, so a second code arrives from a different sender than - the first and leaves the cardholder holding two codes from two - strangers, deciding which to read back. - example: false diff --git a/openapi/components/schemas/care/CareChatVerificationResponse.yaml b/openapi/components/schemas/care/CareChatVerificationResponse.yaml deleted file mode 100644 index 819b38ec2..000000000 --- a/openapi/components/schemas/care/CareChatVerificationResponse.yaml +++ /dev/null @@ -1,31 +0,0 @@ -type: object -description: >- - Acknowledgement that a verification code is on its way to the cardholder. - `202`, not `200`: the SMS is in flight, not delivered. -required: - - status - - destinationMasked - - alreadySent -properties: - status: - type: string - enum: - - SENT - description: >- - Always `SENT`. The chat's own state is reported as - `verificationStatus` on the message route. - example: SENT - destinationMasked: - type: string - description: >- - The masked phone number the code is going to, for a UI to show as "we - texted ...". Always the number already on file for the cardholder; the - caller cannot nominate a destination. - example: '***0001' - alreadySent: - type: boolean - description: >- - True when a code was already outstanding and this call sent no new SMS. - Show "we already texted you" rather than implying a fresh send. A - request with `resend: true` sends a new code and returns false. - example: true diff --git a/openapi/components/schemas/care/CareChatVerificationStatus.yaml b/openapi/components/schemas/care/CareChatVerificationStatus.yaml index 2462369df..eb52bcdcb 100644 --- a/openapi/components/schemas/care/CareChatVerificationStatus.yaml +++ b/openapi/components/schemas/care/CareChatVerificationStatus.yaml @@ -29,6 +29,6 @@ description: | |--------|-------------| | `PENDING` | No code has been sent on this chat yet. This is where every new chat starts. | | `SENT` | A code has been sent to the cardholder and is outstanding. | - | `VERIFIED` | The cardholder answered the code, either at `POST /care/chats/{chatId}/verifications/confirm` or by reading it back to the agent. Account-reaching tools are available for as long as that answer is newer than the session lifetime, and a session token was issued by whichever of the two doors took the code. | + | `VERIFIED` | The cardholder sent the code as a message and the agent confirmed it. Account-reaching tools are available for as long as that answer is newer than the session lifetime, and the session token was issued on the message response to that turn. | | `TRUSTED` | The cardholder is treated as present without an SMS code on this chat. Reserved for channels Grid can already attribute; a partner chat does not start here. Bounded by the same freshness window as `VERIFIED`. | - | `FAILED` | The last code was answered incorrectly or expired. Send another code with `POST /care/chats/{chatId}/verifications`. | + | `FAILED` | The last code was answered incorrectly or expired. The cardholder can send the code again, or ask the agent for a new one. | diff --git a/openapi/components/schemas/care/CareError.yaml b/openapi/components/schemas/care/CareError.yaml index 00c0c959b..ca66c33cd 100644 --- a/openapi/components/schemas/care/CareError.yaml +++ b/openapi/components/schemas/care/CareError.yaml @@ -22,11 +22,10 @@ properties: | Error Code | HTTP | Description | |------------|------|-------------| - | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `customerId` that is not a `Customer` id, a code of the wrong shape, or a chat that cannot be verified at all. | + | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `customerId` that is not a `Customer` id, or a chat that cannot be verified at all. | | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused at the content boundary and no turn ran. `reason` carries the sentence to show them. | - | `CARE_VERIFICATION_FAILED` | 400 | The code did not match, or the chat's existing verification is older than the session lifetime and so cannot mint another session. Send a new code and confirm it, or send the next message and let the agent ask for one. | | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified — that is signalled in a `200` body. | - | `CARE_SESSION_EXPIRED` | 401 | The chat's verification is still current, so it requires `X-Ls-Care-Session`, and the header was missing; or the token presented was malformed or past its expiry. Send the cardholder a new code and confirm it. Once the chat's own verification has aged past the session lifetime this error stops: the header is no longer required, and the next message sends a fresh code instead of being refused. | + | `CARE_SESSION_EXPIRED` | 401 | The chat's verification is still current, so it requires `X-Ls-Care-Session`, and the header was missing; or the token presented was malformed or past its expiry. Retry the message with no `X-Ls-Care-Session` header. If that is refused too, the token for a live verification has been lost: close the chat and send the message to the cardholder as `Customer:`, which opens a fresh chat and sends a new code. Once the chat's own verification has aged past the session lifetime this error stops: the header is no longer required, and the next message sends a fresh code instead of being refused. | | `FORBIDDEN` | 403 | Embedded care is not enabled for this platform. | | `CARE_SESSION_PLATFORM_MISMATCH` | 403 | The session token was minted for a different platform. Stop; retrying will not help. | | `NOT_FOUND` | 404 | No such chat for this platform. A chat belonging to another platform, a chat that never existed, a malformed chat id, and an `X-Ls-Care-Session` token minted for a different chat or a different cardholder are one answer. | @@ -44,7 +43,6 @@ properties: enum: - INVALID_INPUT - CARE_MESSAGE_REJECTED - - CARE_VERIFICATION_FAILED - UNAUTHORIZED - CARE_SESSION_EXPIRED - FORBIDDEN diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index bf50ba7c7..6f35111ff 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -103,37 +103,37 @@ tags: from the partner's own app. The platform calls with its ordinary Grid credential, but the person typing is the cardholder rather than the caller, so reaching that cardholder's account takes a second proof: an - SMS code they answer, which the partner afterwards carries in the - `X-Ls-Care-Session` header. + SMS code they send into the chat, after which the partner carries a + session token in the `X-Ls-Care-Session` header. - **The surface is four routes, and the happy path is two calls.** + **The surface is two routes, and verification happens inside the + conversation.** 1. **Open the chat.** `POST /care/chats/{chatId}/messages` with an empty `message`, addressed to the cardholder as `Customer:`. Returns the chat's id and the agent's `greeting`, which is what a chat window renders before anybody has typed. Sends no SMS, runs no turn, bills - nothing, and is safe to call again — a widget that reloaded gets the + nothing, and is safe to call again: a widget that reloaded gets the same chat back, this time without a greeting. 2. **Send the cardholder's first message.** The same route with their text and an `Idempotency-Key` header. One turn runs synchronously and the agent's answer comes back in `reply`. On a chat that cannot yet reach the account this call also texts the verification code and says so in the same body: `verificationRequired: true`, with - `destinationMasked`, `alreadySent` and `codeLength` for the input you - put on screen. - 3. **Confirm the code.** - `POST /care/chats/{chatId}/verifications/confirm` returns a - `sessionToken` and its `expiresAt`. Send that token as the - `X-Ls-Care-Session` header on every message from then on. A - cardholder who reads the code back to the agent as an ordinary - message instead is confirmed the same way, and that turn's own - response carries the same two fields. + `destinationMasked`, `alreadySent` and `codeLength`, and the agent's + `reply` asks the cardholder for the code. + 3. **The cardholder sends the code as an ordinary message.** The agent + checks it with the SMS provider, and the response to that turn + carries `sessionToken` and `expiresAt`. Send that token as the + `X-Ls-Care-Session` header on every message from then on. A code that + does not match is answered in `reply`, and the cardholder can simply + send it again. 4. **Close the chat** with `DELETE /care/chats/{chatId}` when the cardholder is done. - `POST /care/chats/{chatId}/verifications` is the *Send another code* - button rather than a step in the flow: step 2 already sent the first - code. + **A cardholder whose code never arrived asks the agent for another + one.** The agent sends a new code to the number on file and says so in + its `reply`. There is no separate route for it. Step 1 is optional. A message addressed to a `Customer:` id opens that cardholder's chat if they have none, so a partner whose cardholder has @@ -151,13 +151,16 @@ tags: than the session lifetime, fifteen minutes in the default deployment, which is also how long a session token lives. Past that window the chat stops requiring the header: a message sent without one gets a fresh - code, and the agent asks the cardholder to read it back inside the + code, and the agent asks the cardholder to send it back inside the conversation, which hands the partner the next session token. Sending the dead token anyway is still a `401 CARE_SESSION_EXPIRED`, so the handling is to discard the token on that error and resend the message with no `X-Ls-Care-Session` header rather than to tear the chat down. If that resend is refused too, the chat's verification is still current - and the way back is a new code. + and its token has been lost. Close the chat with + `DELETE /care/chats/{chatId}`, which needs no token, and send the + message to the cardholder as `Customer:`: that opens a fresh chat + and sends a new code. Two things an integration has to get right: @@ -168,10 +171,9 @@ tags: and into every log the transcript reaches. - **The session token is returned once and is never re-readable.** No endpoint hands it back, there is no revocation, and it is short-lived - by design. Hold it for the life of the cardholder's session; a partner - that drops it has to send a new code and confirm it again. It arrives - from whichever door took the code, so read `sessionToken` on the - message response as well as on the confirm response. + by design. Hold it for the life of the cardholder's session. It + arrives on the message response to the turn in which the cardholder + sent their code, and on no other. Some things are absent by construction, so no integration can come to depend on them: nothing returns a transcript or an earlier turn's reply @@ -477,10 +479,6 @@ paths: $ref: paths/stablecoins/stablecoin-provider-accounts_{stablecoinProviderAccountId}.yaml /care/chats/{chatId}/messages: $ref: paths/care/care_chats_{chatId}_messages.yaml - /care/chats/{chatId}/verifications: - $ref: paths/care/care_chats_{chatId}_verifications.yaml - /care/chats/{chatId}/verifications/confirm: - $ref: paths/care/care_chats_{chatId}_verifications_confirm.yaml /care/chats/{chatId}: $ref: paths/care/care_chats_{chatId}.yaml webhooks: diff --git a/openapi/paths/care/care_chats_{chatId}_messages.yaml b/openapi/paths/care/care_chats_{chatId}_messages.yaml index bc7b369ff..3eebf9872 100644 --- a/openapi/paths/care/care_chats_{chatId}_messages.yaml +++ b/openapi/paths/care/care_chats_{chatId}_messages.yaml @@ -48,8 +48,6 @@ post: `codeLength` come back with it, and the agent's own sentence in `reply` asks the cardholder for the code. Later messages on the same chat find the code already outstanding and send nothing, reporting `alreadySent: true`. - Collect the code and post it to - `POST /care/chats/{chatId}/verifications/confirm`. **Render `displayMessage`, not your own copy of the input.** Grid redacts the cardholder's text on the way in — a card number they pasted comes back @@ -57,26 +55,23 @@ post: own input box instead has put an unmasked PAN in the transcript, on the screen and in whatever logs the transcript reaches. - **The code can also be read back to the agent, and then this route issues - the session.** When the cardholder types their code as a message rather - than into your code input, the agent puts it to the same provider, through - the same check `POST /care/chats/{chatId}/verifications/confirm` performs, - and the chat comes out of that turn able to reach the account. That turn's - body carries `sessionToken` and `expiresAt`, under the same names and with - the same meaning as the confirm route's, so one handler in your app serves - both doors. 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, or you will hold a chat that is privileged and unusable at - once. + **The cardholder completes verification by sending the code as an + ordinary message, and that turn's response carries the session.** The + agent checks the code with the SMS provider, and the chat comes out of + that turn able to reach the account. 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, or you will hold a chat that is privileged and + unusable at once. A code that does not match is answered in `reply`, not + as an error, and the cardholder can send it again. - The structured route is not deprecated by this, and it is still the one to - call when your own UI collects the code: only it can tell you where the - code went, whether one was already in flight, and how many digits it has. + **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, every message must carry the - session token in the `X-Ls-Care-Session` header, whichever of the two - doors issued it. Without it the call is refused with - `CARE_SESSION_EXPIRED`. + session token in the `X-Ls-Care-Session` header. Without it the call is + refused with `CARE_SESSION_EXPIRED`. **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 @@ -87,18 +82,20 @@ post: being required, and the next message **sent without it** is treated as an unverified one: a fresh code goes out, the body carries `verificationRequired: true` with `destinationMasked`, `alreadySent` and - `codeLength`, and the agent's own `reply` asks the cardholder to read the - code back. The turn still runs and still answers. + `codeLength`, and the agent's own `reply` asks the cardholder to send the + code. The turn still runs and still answers. Sending the expired token anyway is still a `401 CARE_SESSION_EXPIRED`. One recovery covers a token that was lost, one that was never valid and one that has expired: retry the message with no `X-Ls-Care-Session` header. On a chat whose verification has gone stale that call succeeds, sends a fresh code and has the agent ask for it, and the turn in which the - cardholder reads it back returns the next `sessionToken`. On a chat whose + cardholder sends it returns the next `sessionToken`. On a chat whose verification is still current it is refused with `CARE_SESSION_EXPIRED` - again, which is the signal to send a code with - `POST /care/chats/{chatId}/verifications` and confirm it. An empty message + again, which means the token for a live verification has been lost: close + the chat with `DELETE /care/chats/{chatId}`, which needs no token, and + send the message to the cardholder as `Customer:`, which opens a + fresh chat and sends a new code. 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. @@ -136,10 +133,8 @@ post: in: header required: false description: >- - The `sessionToken` from - `POST /care/chats/{chatId}/verifications/confirm`, or the one this - route returned on the turn that verified the cardholder in the - conversation. Required on every message while the chat's verification + The `sessionToken` this route returned on the turn in which the + cardholder sent their verification code. Required on every message while the chat's verification is current, and refused with `CARE_SESSION_EXPIRED` when it is missing there. Not required before the cardholder has verified, nor once the chat's verification has aged past the session lifetime; but a token @@ -166,6 +161,10 @@ post: summary: The cardholder's first message, which also sends the code value: message: My card was declined at the grocery store + verificationCode: + summary: The cardholder sends their code, and this turn returns the session + value: + message: The code is 482913 afterVerification: summary: A later message, sent with the X-Ls-Care-Session header value: diff --git a/openapi/paths/care/care_chats_{chatId}_verifications.yaml b/openapi/paths/care/care_chats_{chatId}_verifications.yaml deleted file mode 100644 index d6e397159..000000000 --- a/openapi/paths/care/care_chats_{chatId}_verifications.yaml +++ /dev/null @@ -1,143 +0,0 @@ -parameters: - - name: chatId - in: path - description: >- - The chat to send another code on, as returned in `chatId` by - `POST /care/chats/{chatId}/messages`. - required: true - schema: - type: string - example: CareChat:019542f5-b3e7-1d02-0000-000000000030 -post: - summary: Resend a care chat verification code - description: | - Send the cardholder another verification code. - - **This is the resend, not the first send.** The cardholder's first message - already sent a code and said so in its own body; this is what the *Send - another code* button calls when they say it never arrived. A partner never - has to call it to start verification, and does not have to call it to - recover a chat whose verification has gone stale either, because the next - message sends a code by itself. - - With `resend` false — the default, and the same as sending no body — the - call asks for a code and is content to be told one is already outstanding: - the answer carries `alreadySent: true` and no new SMS goes out. That is a - success. With `resend` true a second code is sent. Ask the cardholder - before doing that: codes are sent from a pool of numbers, so the second - arrives from a different sender than the first, leaving them holding two - codes from two strangers and deciding which to read back. - - The destination is always the number already on file for the cardholder. - There is no way to nominate one, because a code sent to a number the - caller chose would prove nothing about the person holding the card. - - `202`, not `200`: the SMS is in flight, not delivered. - - A chat that is already verified is refused with `INVALID_INPUT` while its - verification is recent — there is nothing left to prove. Once that - verification has aged past the session lifetime the chat no longer reaches - the cardholder's account, so there is something to prove again: the - refusal lifts on its own and this route sends a fresh code, whatever - `verificationStatus` still says. - operationId: resendCareChatVerification - tags: - - Embedded Care - security: - - BasicAuth: [] - requestBody: - required: false - content: - application/json: - schema: - $ref: ../../components/schemas/care/CareChatVerificationRequest.yaml - examples: - askForACode: - summary: Ask for a code, accepting the one already outstanding - value: - resend: false - deliberateResend: - summary: The cardholder says the first code never arrived - value: - resend: true - responses: - '202': - description: >- - The code is on its way, or one was already outstanding and none was - sent — `alreadySent` says which. - content: - application/json: - schema: - $ref: ../../components/schemas/care/CareChatVerificationResponse.yaml - '400': - description: >- - `INVALID_INPUT` when the chat is already verified, when the body - carries an unknown field, or when verification is not available for - this chat at all — the platform's care programme does not offer SMS - verification, or the cardholder record behind the chat could not be - read. - content: - application/json: - schema: - $ref: ../../components/schemas/care/CareError.yaml - '401': - description: '`UNAUTHORIZED` — the platform''s Grid credential is missing or wrong.' - content: - application/json: - schema: - $ref: ../../components/schemas/care/CareError.yaml - '403': - description: '`FORBIDDEN` — embedded care is not enabled for this platform.' - content: - application/json: - schema: - $ref: ../../components/schemas/care/CareError.yaml - '404': - description: >- - `NOT_FOUND` — no such chat for this platform. A chat that belongs to - another platform, one that never existed, and a malformed chat id are - one answer. - content: - application/json: - schema: - $ref: ../../components/schemas/care/CareError.yaml - '409': - description: >- - `CARE_CHAT_ENDED` when the chat has been closed, or `CONFLICT` when it - has no cardholder bound to it. - content: - application/json: - schema: - $ref: ../../components/schemas/care/CareError.yaml - '429': - description: >- - `CARE_BUDGET_EXCEEDED` when this chat has reached its limit, or - `RATE_LIMITED` when the platform is calling this endpoint too often; - back off for the interval in the `Retry-After` header. - content: - application/json: - schema: - $ref: ../../components/schemas/care/CareError.yaml - '500': - description: Internal service error. - content: - application/json: - schema: - $ref: ../../components/schemas/care/CareError.yaml - '502': - description: >- - `CARE_VERIFICATION_UNAVAILABLE` — the SMS provider could not be - reached. The factor is down, so retry rather than asking the - cardholder for anything. - content: - application/json: - schema: - $ref: ../../components/schemas/care/CareError.yaml - '503': - description: >- - `SERVICE_UNAVAILABLE` when embedded care is not available in this - deployment, or the support agent is temporarily turned off. - content: - application/json: - schema: - $ref: ../../components/schemas/care/CareError.yaml diff --git a/openapi/paths/care/care_chats_{chatId}_verifications_confirm.yaml b/openapi/paths/care/care_chats_{chatId}_verifications_confirm.yaml deleted file mode 100644 index abb2923cd..000000000 --- a/openapi/paths/care/care_chats_{chatId}_verifications_confirm.yaml +++ /dev/null @@ -1,144 +0,0 @@ -parameters: - - name: chatId - in: path - description: >- - The chat the code was sent on, as returned in `chatId` by - `POST /care/chats/{chatId}/messages`. - required: true - schema: - type: string - example: CareChat:019542f5-b3e7-1d02-0000-000000000030 -post: - summary: Confirm a care chat verification code - description: | - Check the code the cardholder read back, and hand over the session that - every later message on this chat rides on. - - **The session token is returned here and never again.** No endpoint reads - it back, and there is no revocation — the short expiry is what makes that - acceptable. Keep it in the cardholder's own session on your side, send it - as the `X-Ls-Care-Session` header on every later message, and treat losing - it as re-verification: send a new code with - `POST /care/chats/{chatId}/verifications` and confirm it again. Read - `expiresAt` rather than assuming a lifetime. - - **This is not the only door.** A cardholder can read the code back to the - agent as an ordinary message, and `POST /care/chats/{chatId}/messages` - then returns the same `sessionToken` and `expiresAt` on that turn. Call - this route when your own UI collects the code, because only it can tell - you where the code went, whether one was already in flight, and how many - digits it has. - - The token is bound to this platform, this chat and this cardholder. It - cannot drive another chat, and presenting it to another platform's chat is - refused with `CARE_SESSION_PLATFORM_MISMATCH`. - - A code that does not match is `CARE_VERIFICATION_FAILED` — ask the - cardholder again. A provider that could not be reached is - `CARE_VERIFICATION_UNAVAILABLE` — retry; nothing the cardholder does will - help. The two are kept apart deliberately, because they call for opposite - reactions. - - A chat whose verification is older than the session lifetime will not mint - a fresh session off it: that would be the platform issuing itself a - session for a cardholder who has not shown themselves in longer than a - session lasts. It is refused with `CARE_VERIFICATION_FAILED` before the - provider is reached, and the way forward is a new code. That is the same - window that decides whether the chat reaches the cardholder's account at - all, so a stale chat is not a locked one: sending a code with - `POST /care/chats/{chatId}/verifications`, or simply sending the next - message and letting the agent ask, both open it again. - operationId: confirmCareChatVerification - tags: - - Embedded Care - security: - - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: ../../components/schemas/care/CareChatVerificationConfirmRequest.yaml - example: - code: '482913' - responses: - '200': - description: >- - The cardholder is verified on this chat, and the session token is in - this body — it is not retrievable afterwards. - content: - application/json: - schema: - $ref: ../../components/schemas/care/CareChatSession.yaml - '400': - description: >- - `CARE_VERIFICATION_FAILED` when the code did not match, or when the - chat's existing verification is too old to mint another session. - `INVALID_INPUT` when the code is not of the shape the SMS carries, - when the body is missing or carries an unknown field, or when - verification is not available for this chat. - content: - application/json: - schema: - $ref: ../../components/schemas/care/CareError.yaml - '401': - description: '`UNAUTHORIZED` — the platform''s Grid credential is missing or wrong.' - content: - application/json: - schema: - $ref: ../../components/schemas/care/CareError.yaml - '403': - description: '`FORBIDDEN` — embedded care is not enabled for this platform.' - content: - application/json: - schema: - $ref: ../../components/schemas/care/CareError.yaml - '404': - description: >- - `NOT_FOUND` — no such chat for this platform. A chat that belongs to - another platform, one that never existed, and a malformed chat id are - one answer. - content: - application/json: - schema: - $ref: ../../components/schemas/care/CareError.yaml - '409': - description: >- - `CARE_CHAT_ENDED` when the chat has been closed, or `CONFLICT` when it - has no cardholder bound to it. - content: - application/json: - schema: - $ref: ../../components/schemas/care/CareError.yaml - '429': - description: >- - `CARE_BUDGET_EXCEEDED` when this chat has reached its limit, or - `RATE_LIMITED` when the platform is calling this endpoint too often; - back off for the interval in the `Retry-After` header. - content: - application/json: - schema: - $ref: ../../components/schemas/care/CareError.yaml - '500': - description: Internal service error. - content: - application/json: - schema: - $ref: ../../components/schemas/care/CareError.yaml - '502': - description: >- - `CARE_VERIFICATION_UNAVAILABLE` — the SMS provider could not be - reached, so the code could not be checked. Retry; the cardholder has - done nothing wrong. - content: - application/json: - schema: - $ref: ../../components/schemas/care/CareError.yaml - '503': - description: >- - `SERVICE_UNAVAILABLE` when embedded care is not available in this - deployment, or the support agent is temporarily turned off. - content: - application/json: - schema: - $ref: ../../components/schemas/care/CareError.yaml From 9ab210e16c62b15dc32e0aaebf4185dc0ad8b243 Mon Sep 17 00:00:00 2001 From: kphurley7 Date: Wed, 16 Sep 2026 16:01:19 -0500 Subject: [PATCH 05/11] docs(care): fold the opening line into reply, and re-verify a lost session The opening call's separate greeting object is gone, along with the CareChatGreeting schema and its Stainless model. The call that opens a chat now returns the agent's opening line as reply, with id, displayMessage and replayed absent; a resume carries no reply. The message route and the tag overview state the single rendering rule: displayMessage if present, then reply if present. A message with no session token, or an expired one, on a chat whose verification is still current now verifies the cardholder again inside a 200 instead of a 401: the verification is taken away before the turn runs, a new code is sent and the agent asks for it. Only a malformed token, or one minted for another platform, chat or cardholder, is refused. The tag overview, the X-Ls-Care-Session header, the 401 response, the response fields and the CARE_SESSION_EXPIRED error row say so, and the close and reopen workaround is removed. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015njn7Fen4mg9NDX7AZQRTj --- .stainless/stainless.yml | 1 - mintlify/openapi.yaml | 154 ++++++++---------- openapi.yaml | 154 ++++++++---------- .../schemas/care/CareChatGreeting.yaml | 25 --- .../schemas/care/CareChatMessageResponse.yaml | 43 ++--- .../components/schemas/care/CareError.yaml | 4 +- openapi/openapi.yaml | 46 +++--- .../care/care_chats_{chatId}_messages.yaml | 92 ++++++----- 8 files changed, 237 insertions(+), 282 deletions(-) delete mode 100644 openapi/components/schemas/care/CareChatGreeting.yaml diff --git a/.stainless/stainless.yml b/.stainless/stainless.yml index 7daeea06c..c65a68be9 100644 --- a/.stainless/stainless.yml +++ b/.stainless/stainless.yml @@ -691,7 +691,6 @@ resources: models: care_chat_message_request: "#/components/schemas/CareChatMessageRequest" care_chat_message_response: "#/components/schemas/CareChatMessageResponse" - care_chat_greeting: "#/components/schemas/CareChatGreeting" care_chat_verification_status: "#/components/schemas/CareChatVerificationStatus" care_error: "#/components/schemas/CareError" diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 1d21fec17..b733da154 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -75,10 +75,11 @@ tags: 1. **Open the chat.** `POST /care/chats/{chatId}/messages` with an empty `message`, addressed to the cardholder as `Customer:`. Returns - the chat's id and the agent's `greeting`, which is what a chat window - renders before anybody has typed. Sends no SMS, runs no turn, bills - nothing, and is safe to call again: a widget that reloaded gets the - same chat back, this time without a greeting. + the chat's id and the agent's opening line in `reply`, which is what + a chat window renders before anybody has typed. Sends no SMS, runs no + turn, needs no `Idempotency-Key`, bills nothing, and is safe to call + again: a widget that reloaded gets the same chat back, this time + without a `reply`. 2. **Send the cardholder's first message.** The same route with their text and an `Idempotency-Key` header. One turn runs synchronously and the agent's answer comes back in `reply`. On a chat that cannot yet @@ -99,6 +100,9 @@ tags: one.** The agent sends a new code to the number on file and says so in its `reply`. There is no separate route for it. + **Render `displayMessage` if present, then `reply` if present.** That one + rule covers an opening, a resume and a turn. + Step 1 is optional. A message addressed to a `Customer:` id opens that cardholder's chat if they have none, so a partner whose cardholder has just typed something spends one call rather than two. @@ -107,24 +111,21 @@ tags: reported inside a `200`, because the first message from every new cardholder is unverified and a `401` there would be indistinguishable from a bad credential. On these endpoints a `401` means the platform's - Grid credential is wrong, or a chat whose verification is still current - was driven without its session token. - - **Verification expires, and expiry is recoverable in the chat.** 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 also how long a session token lives. Past that window the chat - stops requiring the header: a message sent without one gets a fresh - code, and the agent asks the cardholder to send it back inside the - conversation, which hands the partner the next session token. Sending - the dead token anyway is still a `401 CARE_SESSION_EXPIRED`, so the - handling is to discard the token on that error and resend the message - with no `X-Ls-Care-Session` header rather than to tear the chat down. - If that resend is refused too, the chat's verification is still current - and its token has been lost. Close the chat with - `DELETE /care/chats/{chatId}`, which needs no token, and send the - message to the cardholder as `Customer:`: that opens a fresh chat - and sends a new code. + Grid credential is wrong, or the session token presented is malformed. + + **Verification expires, and a lost session is recoverable in the + chat.** 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 also how long a session token lives. Past + that window a message gets a fresh code, and the agent asks the + cardholder to send it back inside the conversation, which hands the + partner the next session token. A 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 taken away before the turn runs, a new code is sent, and + the agent asks for it. So a client that lost its token, or held one too + long, simply keeps sending messages. Only a token that is malformed, or + that was minted for another platform, chat or cardholder, is refused. Two things an integration has to get right: @@ -137,7 +138,8 @@ tags: endpoint hands it back, there is no revocation, and it is short-lived by design. Hold it for the life of the cardholder's session. It arrives on the message response to the turn in which the cardholder - sent their code, and on no other. + sent their code, and on no other. Losing it costs the cardholder a new + code, never access to the chat. Some things are absent by construction, so no integration can come to depend on them: nothing returns a transcript or an earlier turn's reply @@ -10677,12 +10679,13 @@ paths: may be sent again and will run a fresh turn. **An empty or absent `message` opens the chat and runs nothing.** On a - cardholder with no open chat it opens one and answers with `greeting`, 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 `greeting` does not, because - a greeting arriving mid-conversation reads as the agent introducing itself - a second time. Either way no code is sent, no `Idempotency-Key` is needed + 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`, `displayMessage` and `replayed` are absent on + both, because nothing was typed and no turn ran. Either way no code is sent, no `Idempotency-Key` is needed or consumed, and nothing is billed — a widget in a reconnect loop may call it as often as it likes. @@ -10696,9 +10699,10 @@ paths: asks the cardholder for the code. Later messages on the same chat find the code already outstanding and send nothing, reporting `alreadySent: true`. - **Render `displayMessage`, not your own copy of the input.** Grid redacts - the cardholder's text on the way in — a card number they pasted comes back - masked — and `displayMessage` is that redacted echo. A UI that echoes its + **Render `displayMessage` if present, then `reply` if present.** That one + rule covers every call: an opening, a resume and a turn. Grid redacts the + cardholder's text on the way in, so a card number they pasted comes back + masked, and `displayMessage` is that redacted echo. A UI that echoes its own input box instead has put an unmasked PAN in the transcript, on the screen and in whatever logs the transcript reaches. @@ -10708,17 +10712,15 @@ paths: that turn able to reach the account. 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, or you will hold a chat that is privileged and - unusable at once. A code that does not match is answered in `reply`, not + 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, every message must carry the - session token in the `X-Ls-Care-Session` header. Without it the call is - refused with `CARE_SESSION_EXPIRED`. + 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 @@ -10732,20 +10734,25 @@ paths: `codeLength`, and the agent's own `reply` asks the cardholder to send the code. The turn still runs and still answers. - Sending the expired token anyway is still a `401 CARE_SESSION_EXPIRED`. - One recovery covers a token that was lost, one that was never valid and - one that has expired: retry the message with no `X-Ls-Care-Session` - header. On a chat whose verification has gone stale that call succeeds, - sends a fresh code and has the agent ask for it, and the turn in which the - cardholder sends it returns the next `sessionToken`. On a chat whose - verification is still current it is refused with `CARE_SESSION_EXPIRED` - again, which means the token for a live verification has been lost: close - the chat with `DELETE /care/chats/{chatId}`, which needs no token, and - send the message to the cardholder as `Customer:`, which opens a - fresh chat and sends a new code. 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. + **A lost or expired session token verifies again rather than being + refused.** A 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 chat's verification is taken away 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 body + carries `verificationRequired: true` with `destinationMasked`, + `alreadySent` and `codeLength`, the agent asks the cardholder for the + code, 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 describes the chat as of the moment the turn started, `verificationStatus` included. The agent can verify a cardholder @@ -10771,7 +10778,7 @@ paths: - name: X-Ls-Care-Session in: header required: false - description: The `sessionToken` this route returned on the turn in which the cardholder sent their verification code. Required on every message while the chat's verification is current, and refused with `CARE_SESSION_EXPIRED` when it is missing there. Not required before the cardholder has verified, nor once the chat's verification has aged past the session lifetime; but a token that is sent is always checked, so a malformed or expired one is refused with `CARE_SESSION_EXPIRED` whatever state the chat is in, and one minted for another chat or another cardholder is `NOT_FOUND`. An opening never reads the header at all, because a widget that reloaded has lost its token and that is the case the opening exists for. + description: 'The `sessionToken` this route returned on the turn in which the cardholder sent their verification code. Send it on every message while the chat''s verification is current. A message without it, or with one past its expiry, is not refused: the chat''s verification is taken away before the turn runs and a new code is sent, so the cardholder verifies again in the conversation. A token that is sent is always checked for what it names: a malformed one is refused with `CARE_SESSION_EXPIRED`, one minted for another chat or another cardholder is `NOT_FOUND`, and one minted for another platform is `CARE_SESSION_PLATFORM_MISMATCH`, whether or not it has expired. An opening never reads the header at all, because a widget that reloaded has lost its token and that is the case the opening exists for.' schema: type: string example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl @@ -10783,7 +10790,7 @@ paths: $ref: '#/components/schemas/CareChatMessageRequest' examples: openTheChat: - summary: Open or resume the cardholder's chat, saying nothing + summary: Open or resume the cardholder's chat; an opening returns the agent's opening line as reply value: message: '' firstMessage: @@ -10812,7 +10819,7 @@ paths: schema: $ref: '#/components/schemas/CareError' '401': - description: '`UNAUTHORIZED` when the platform''s Grid credential is missing or wrong. `CARE_SESSION_EXPIRED` when the chat''s verification is still current and the `X-Ls-Care-Session` header is missing, or when the token presented is malformed or past its expiry. An unverified cardholder is never a `401`; that is a `200` carrying `verificationRequired`. Neither is a chat whose verification has gone stale, which is asked for a code again inside a `200`.' + description: '`UNAUTHORIZED` when the platform''s Grid credential is missing or wrong. `CARE_SESSION_EXPIRED` when the `X-Ls-Care-Session` token presented is malformed. A missing or expired session token is never a `401`: it 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.' content: application/json: schema: @@ -27742,30 +27749,9 @@ components: | `VERIFIED` | The cardholder sent the code as a message and the agent confirmed it. Account-reaching tools are available for as long as that answer is newer than the session lifetime, and the session token was issued on the message response to that turn. | | `TRUSTED` | The cardholder is treated as present without an SMS code on this chat. Reserved for channels Grid can already attribute; a partner chat does not start here. Bounded by the same freshness window as `VERIFIED`. | | `FAILED` | The last code was answered incorrectly or expired. The cardholder can send the code again, or ask the agent for a new one. | - CareChatGreeting: - type: object - description: 'The agent''s opening line, recorded as the chat''s first message. Returned only by the call that opened the chat, never on a resume: a greeting arriving mid-conversation reads as the agent introducing itself a second time. Absent when the agent produced no opening line.' - required: - - id - - content - - createdAt - properties: - id: - type: string - description: Id of the recorded greeting message. - example: CareMessage:019542f5-b3e7-1d02-0000-000000000031 - content: - type: string - description: The greeting text to render in the transcript, in the chat's locale. - example: Hi, I'm the support assistant. What can I help you with today? - createdAt: - type: string - format: date-time - description: ISO 8601 timestamp, in UTC, of when the greeting was recorded. - example: '2026-09-12T14:10:00.123456Z' CareChatMessageResponse: type: object - description: 'The result of one call to the message route. Which fields are present depends on what the call did: a turn adds `id`, `displayMessage`, `reply` and `replayed`; the call that opened the chat adds `greeting`; an outstanding verification adds `destinationMasked`, `alreadySent` and, where the service could be read, `codeLength`; and the turn in which the cardholder sent their verification code adds `sessionToken` and `expiresAt`.' + description: 'The result of one call to the message route. Which fields are present depends on what the call did: a turn adds `id`, `displayMessage`, `reply` and `replayed`; the call that opened the chat adds `reply` alone, carrying the agent''s opening line; an outstanding verification adds `destinationMasked`, `alreadySent` and, where the service could be read, `codeLength`; and the turn in which the cardholder sent their verification code adds `sessionToken` and `expiresAt`. Render `displayMessage` if present, then `reply` if present.' required: - chatId - verificationStatus @@ -27780,7 +27766,7 @@ components: verificationRequired: type: boolean description: |- - Whether the cardholder still has to prove they are present before the agent can reach their account. Present on every response, true or false, so a client reads a field rather than an absence. When true, `destinationMasked` and `alreadySent` are present, and the cardholder completes verification by sending the code as an ordinary message. A verification that is outstanding is never reported as a `401`: on this surface a `401` means the platform credential is wrong, or a chat whose verification is still current was driven without its session token. + Whether the cardholder still has to prove they are present before the agent can reach their account. Present on every response, true or false, so a client reads a field rather than an absence. When true, `destinationMasked` and `alreadySent` are present, and the cardholder completes verification by sending the code as an ordinary message. A verification that is outstanding, or one taken away because a message arrived without a live session token, is never reported as a `401`: on this surface a `401` means the platform credential is wrong, or the session token presented is malformed. Read this field rather than `verificationStatus`. A chat whose verification has aged past the session lifetime reports `verificationStatus: VERIFIED` and `verificationRequired: true` at the same time, and that combination is the truthful one: the column records that a code was once answered, and this field records whether the agent can act on the account now. @@ -27796,14 +27782,12 @@ components: example: My card [redacted card number ending 1111] was declined reply: type: string - description: The agent's answer to this message, to render as the assistant's turn. Present only when a turn ran. 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. - example: I can help with that. To look at your account I've texted you a code — what does it say? + description: '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`, consumes none and bills nothing, even though it carries a `reply`. 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.' + example: I can help with that. To look at your account I've texted you a code. What does it say? replayed: type: boolean description: Whether this answer came from the record of an earlier call under the same `Idempotency-Key` rather than from a turn run now. Present only when a turn ran. There is no route that reads a transcript, so a retry under the original key is the only way to recover a reply that was produced but never delivered. example: false - greeting: - $ref: '#/components/schemas/CareChatGreeting' destinationMasked: type: string description: The masked phone number the code was sent to, for a UI to show as "we texted ...". Present whenever `verificationRequired` is true. The destination is the number already on file for the cardholder and cannot be nominated by the caller. @@ -27821,7 +27805,7 @@ components: description: |- 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. The agent checks it with the SMS provider, and the chat comes out of that turn able to reach the account. This field is how the partner is handed the credential that turn earned, and it is the only place a session token is ever issued. Without it you would hold a chat that is privileged and unusable at once, because every later message on it is refused with `CARE_SESSION_EXPIRED`. + The cardholder completes verification by sending the SMS code as an ordinary message. The agent checks it with the SMS provider, and the chat comes out of that turn able to reach the account. This field is how the partner is handed the credential that turn earned, and it is the only place a session token is ever issued. A later message sent without it takes the verification away again 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, there is no revocation, 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`. @@ -27830,7 +27814,7 @@ components: expiresAt: type: string format: date-time - description: ISO 8601 timestamp, in UTC, after which the `sessionToken` beside it is refused with `CARE_SESSION_EXPIRED`. Present exactly when `sessionToken` is present. Read it rather than assuming a duration. + description: '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.' example: '2026-09-12T14:25:00Z' CareError: type: object @@ -27860,7 +27844,7 @@ components: | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `customerId` that is not a `Customer` id, or a chat that cannot be verified at all. | | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused at the content boundary and no turn ran. `reason` carries the sentence to show them. | | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified — that is signalled in a `200` body. | - | `CARE_SESSION_EXPIRED` | 401 | The chat's verification is still current, so it requires `X-Ls-Care-Session`, and the header was missing; or the token presented was malformed or past its expiry. Retry the message with no `X-Ls-Care-Session` header. If that is refused too, the token for a live verification has been lost: close the chat and send the message to the cardholder as `Customer:`, which opens a fresh chat and sends a new code. Once the chat's own verification has aged past the session lifetime this error stops: the header is no longer required, and the next message sends a fresh code instead of being refused. | + | `CARE_SESSION_EXPIRED` | 401 | The `X-Ls-Care-Session` token presented is malformed, or not a care session token at all. A missing or expired token is never this error: the message verifies the cardholder again inside a `200` instead. Rarely, also returned when a verification lands on the chat at the same moment a message without a live token takes it away; send the message again. | | `FORBIDDEN` | 403 | Embedded care is not enabled for this platform. | | `CARE_SESSION_PLATFORM_MISMATCH` | 403 | The session token was minted for a different platform. Stop; retrying will not help. | | `NOT_FOUND` | 404 | No such chat for this platform. A chat belonging to another platform, a chat that never existed, a malformed chat id, and an `X-Ls-Care-Session` token minted for a different chat or a different cardholder are one answer. | @@ -27898,7 +27882,7 @@ components: reason: type: string description: Human-readable explanation, for a log or a support ticket. Not for branching, and not always safe to show a cardholder verbatim — the exception is `CARE_MESSAGE_REJECTED`, whose `reason` is written to be shown. - example: The care session has expired. Verify the cardholder again. + example: The care session token is not valid. WebhookType: type: string enum: diff --git a/openapi.yaml b/openapi.yaml index 1d21fec17..b733da154 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -75,10 +75,11 @@ tags: 1. **Open the chat.** `POST /care/chats/{chatId}/messages` with an empty `message`, addressed to the cardholder as `Customer:`. Returns - the chat's id and the agent's `greeting`, which is what a chat window - renders before anybody has typed. Sends no SMS, runs no turn, bills - nothing, and is safe to call again: a widget that reloaded gets the - same chat back, this time without a greeting. + the chat's id and the agent's opening line in `reply`, which is what + a chat window renders before anybody has typed. Sends no SMS, runs no + turn, needs no `Idempotency-Key`, bills nothing, and is safe to call + again: a widget that reloaded gets the same chat back, this time + without a `reply`. 2. **Send the cardholder's first message.** The same route with their text and an `Idempotency-Key` header. One turn runs synchronously and the agent's answer comes back in `reply`. On a chat that cannot yet @@ -99,6 +100,9 @@ tags: one.** The agent sends a new code to the number on file and says so in its `reply`. There is no separate route for it. + **Render `displayMessage` if present, then `reply` if present.** That one + rule covers an opening, a resume and a turn. + Step 1 is optional. A message addressed to a `Customer:` id opens that cardholder's chat if they have none, so a partner whose cardholder has just typed something spends one call rather than two. @@ -107,24 +111,21 @@ tags: reported inside a `200`, because the first message from every new cardholder is unverified and a `401` there would be indistinguishable from a bad credential. On these endpoints a `401` means the platform's - Grid credential is wrong, or a chat whose verification is still current - was driven without its session token. - - **Verification expires, and expiry is recoverable in the chat.** 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 also how long a session token lives. Past that window the chat - stops requiring the header: a message sent without one gets a fresh - code, and the agent asks the cardholder to send it back inside the - conversation, which hands the partner the next session token. Sending - the dead token anyway is still a `401 CARE_SESSION_EXPIRED`, so the - handling is to discard the token on that error and resend the message - with no `X-Ls-Care-Session` header rather than to tear the chat down. - If that resend is refused too, the chat's verification is still current - and its token has been lost. Close the chat with - `DELETE /care/chats/{chatId}`, which needs no token, and send the - message to the cardholder as `Customer:`: that opens a fresh chat - and sends a new code. + Grid credential is wrong, or the session token presented is malformed. + + **Verification expires, and a lost session is recoverable in the + chat.** 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 also how long a session token lives. Past + that window a message gets a fresh code, and the agent asks the + cardholder to send it back inside the conversation, which hands the + partner the next session token. A 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 taken away before the turn runs, a new code is sent, and + the agent asks for it. So a client that lost its token, or held one too + long, simply keeps sending messages. Only a token that is malformed, or + that was minted for another platform, chat or cardholder, is refused. Two things an integration has to get right: @@ -137,7 +138,8 @@ tags: endpoint hands it back, there is no revocation, and it is short-lived by design. Hold it for the life of the cardholder's session. It arrives on the message response to the turn in which the cardholder - sent their code, and on no other. + sent their code, and on no other. Losing it costs the cardholder a new + code, never access to the chat. Some things are absent by construction, so no integration can come to depend on them: nothing returns a transcript or an earlier turn's reply @@ -10677,12 +10679,13 @@ paths: may be sent again and will run a fresh turn. **An empty or absent `message` opens the chat and runs nothing.** On a - cardholder with no open chat it opens one and answers with `greeting`, 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 `greeting` does not, because - a greeting arriving mid-conversation reads as the agent introducing itself - a second time. Either way no code is sent, no `Idempotency-Key` is needed + 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`, `displayMessage` and `replayed` are absent on + both, because nothing was typed and no turn ran. Either way no code is sent, no `Idempotency-Key` is needed or consumed, and nothing is billed — a widget in a reconnect loop may call it as often as it likes. @@ -10696,9 +10699,10 @@ paths: asks the cardholder for the code. Later messages on the same chat find the code already outstanding and send nothing, reporting `alreadySent: true`. - **Render `displayMessage`, not your own copy of the input.** Grid redacts - the cardholder's text on the way in — a card number they pasted comes back - masked — and `displayMessage` is that redacted echo. A UI that echoes its + **Render `displayMessage` if present, then `reply` if present.** That one + rule covers every call: an opening, a resume and a turn. Grid redacts the + cardholder's text on the way in, so a card number they pasted comes back + masked, and `displayMessage` is that redacted echo. A UI that echoes its own input box instead has put an unmasked PAN in the transcript, on the screen and in whatever logs the transcript reaches. @@ -10708,17 +10712,15 @@ paths: that turn able to reach the account. 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, or you will hold a chat that is privileged and - unusable at once. A code that does not match is answered in `reply`, not + 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, every message must carry the - session token in the `X-Ls-Care-Session` header. Without it the call is - refused with `CARE_SESSION_EXPIRED`. + 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 @@ -10732,20 +10734,25 @@ paths: `codeLength`, and the agent's own `reply` asks the cardholder to send the code. The turn still runs and still answers. - Sending the expired token anyway is still a `401 CARE_SESSION_EXPIRED`. - One recovery covers a token that was lost, one that was never valid and - one that has expired: retry the message with no `X-Ls-Care-Session` - header. On a chat whose verification has gone stale that call succeeds, - sends a fresh code and has the agent ask for it, and the turn in which the - cardholder sends it returns the next `sessionToken`. On a chat whose - verification is still current it is refused with `CARE_SESSION_EXPIRED` - again, which means the token for a live verification has been lost: close - the chat with `DELETE /care/chats/{chatId}`, which needs no token, and - send the message to the cardholder as `Customer:`, which opens a - fresh chat and sends a new code. 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. + **A lost or expired session token verifies again rather than being + refused.** A 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 chat's verification is taken away 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 body + carries `verificationRequired: true` with `destinationMasked`, + `alreadySent` and `codeLength`, the agent asks the cardholder for the + code, 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 describes the chat as of the moment the turn started, `verificationStatus` included. The agent can verify a cardholder @@ -10771,7 +10778,7 @@ paths: - name: X-Ls-Care-Session in: header required: false - description: The `sessionToken` this route returned on the turn in which the cardholder sent their verification code. Required on every message while the chat's verification is current, and refused with `CARE_SESSION_EXPIRED` when it is missing there. Not required before the cardholder has verified, nor once the chat's verification has aged past the session lifetime; but a token that is sent is always checked, so a malformed or expired one is refused with `CARE_SESSION_EXPIRED` whatever state the chat is in, and one minted for another chat or another cardholder is `NOT_FOUND`. An opening never reads the header at all, because a widget that reloaded has lost its token and that is the case the opening exists for. + description: 'The `sessionToken` this route returned on the turn in which the cardholder sent their verification code. Send it on every message while the chat''s verification is current. A message without it, or with one past its expiry, is not refused: the chat''s verification is taken away before the turn runs and a new code is sent, so the cardholder verifies again in the conversation. A token that is sent is always checked for what it names: a malformed one is refused with `CARE_SESSION_EXPIRED`, one minted for another chat or another cardholder is `NOT_FOUND`, and one minted for another platform is `CARE_SESSION_PLATFORM_MISMATCH`, whether or not it has expired. An opening never reads the header at all, because a widget that reloaded has lost its token and that is the case the opening exists for.' schema: type: string example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl @@ -10783,7 +10790,7 @@ paths: $ref: '#/components/schemas/CareChatMessageRequest' examples: openTheChat: - summary: Open or resume the cardholder's chat, saying nothing + summary: Open or resume the cardholder's chat; an opening returns the agent's opening line as reply value: message: '' firstMessage: @@ -10812,7 +10819,7 @@ paths: schema: $ref: '#/components/schemas/CareError' '401': - description: '`UNAUTHORIZED` when the platform''s Grid credential is missing or wrong. `CARE_SESSION_EXPIRED` when the chat''s verification is still current and the `X-Ls-Care-Session` header is missing, or when the token presented is malformed or past its expiry. An unverified cardholder is never a `401`; that is a `200` carrying `verificationRequired`. Neither is a chat whose verification has gone stale, which is asked for a code again inside a `200`.' + description: '`UNAUTHORIZED` when the platform''s Grid credential is missing or wrong. `CARE_SESSION_EXPIRED` when the `X-Ls-Care-Session` token presented is malformed. A missing or expired session token is never a `401`: it 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.' content: application/json: schema: @@ -27742,30 +27749,9 @@ components: | `VERIFIED` | The cardholder sent the code as a message and the agent confirmed it. Account-reaching tools are available for as long as that answer is newer than the session lifetime, and the session token was issued on the message response to that turn. | | `TRUSTED` | The cardholder is treated as present without an SMS code on this chat. Reserved for channels Grid can already attribute; a partner chat does not start here. Bounded by the same freshness window as `VERIFIED`. | | `FAILED` | The last code was answered incorrectly or expired. The cardholder can send the code again, or ask the agent for a new one. | - CareChatGreeting: - type: object - description: 'The agent''s opening line, recorded as the chat''s first message. Returned only by the call that opened the chat, never on a resume: a greeting arriving mid-conversation reads as the agent introducing itself a second time. Absent when the agent produced no opening line.' - required: - - id - - content - - createdAt - properties: - id: - type: string - description: Id of the recorded greeting message. - example: CareMessage:019542f5-b3e7-1d02-0000-000000000031 - content: - type: string - description: The greeting text to render in the transcript, in the chat's locale. - example: Hi, I'm the support assistant. What can I help you with today? - createdAt: - type: string - format: date-time - description: ISO 8601 timestamp, in UTC, of when the greeting was recorded. - example: '2026-09-12T14:10:00.123456Z' CareChatMessageResponse: type: object - description: 'The result of one call to the message route. Which fields are present depends on what the call did: a turn adds `id`, `displayMessage`, `reply` and `replayed`; the call that opened the chat adds `greeting`; an outstanding verification adds `destinationMasked`, `alreadySent` and, where the service could be read, `codeLength`; and the turn in which the cardholder sent their verification code adds `sessionToken` and `expiresAt`.' + description: 'The result of one call to the message route. Which fields are present depends on what the call did: a turn adds `id`, `displayMessage`, `reply` and `replayed`; the call that opened the chat adds `reply` alone, carrying the agent''s opening line; an outstanding verification adds `destinationMasked`, `alreadySent` and, where the service could be read, `codeLength`; and the turn in which the cardholder sent their verification code adds `sessionToken` and `expiresAt`. Render `displayMessage` if present, then `reply` if present.' required: - chatId - verificationStatus @@ -27780,7 +27766,7 @@ components: verificationRequired: type: boolean description: |- - Whether the cardholder still has to prove they are present before the agent can reach their account. Present on every response, true or false, so a client reads a field rather than an absence. When true, `destinationMasked` and `alreadySent` are present, and the cardholder completes verification by sending the code as an ordinary message. A verification that is outstanding is never reported as a `401`: on this surface a `401` means the platform credential is wrong, or a chat whose verification is still current was driven without its session token. + Whether the cardholder still has to prove they are present before the agent can reach their account. Present on every response, true or false, so a client reads a field rather than an absence. When true, `destinationMasked` and `alreadySent` are present, and the cardholder completes verification by sending the code as an ordinary message. A verification that is outstanding, or one taken away because a message arrived without a live session token, is never reported as a `401`: on this surface a `401` means the platform credential is wrong, or the session token presented is malformed. Read this field rather than `verificationStatus`. A chat whose verification has aged past the session lifetime reports `verificationStatus: VERIFIED` and `verificationRequired: true` at the same time, and that combination is the truthful one: the column records that a code was once answered, and this field records whether the agent can act on the account now. @@ -27796,14 +27782,12 @@ components: example: My card [redacted card number ending 1111] was declined reply: type: string - description: The agent's answer to this message, to render as the assistant's turn. Present only when a turn ran. 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. - example: I can help with that. To look at your account I've texted you a code — what does it say? + description: '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`, consumes none and bills nothing, even though it carries a `reply`. 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.' + example: I can help with that. To look at your account I've texted you a code. What does it say? replayed: type: boolean description: Whether this answer came from the record of an earlier call under the same `Idempotency-Key` rather than from a turn run now. Present only when a turn ran. There is no route that reads a transcript, so a retry under the original key is the only way to recover a reply that was produced but never delivered. example: false - greeting: - $ref: '#/components/schemas/CareChatGreeting' destinationMasked: type: string description: The masked phone number the code was sent to, for a UI to show as "we texted ...". Present whenever `verificationRequired` is true. The destination is the number already on file for the cardholder and cannot be nominated by the caller. @@ -27821,7 +27805,7 @@ components: description: |- 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. The agent checks it with the SMS provider, and the chat comes out of that turn able to reach the account. This field is how the partner is handed the credential that turn earned, and it is the only place a session token is ever issued. Without it you would hold a chat that is privileged and unusable at once, because every later message on it is refused with `CARE_SESSION_EXPIRED`. + The cardholder completes verification by sending the SMS code as an ordinary message. The agent checks it with the SMS provider, and the chat comes out of that turn able to reach the account. This field is how the partner is handed the credential that turn earned, and it is the only place a session token is ever issued. A later message sent without it takes the verification away again 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, there is no revocation, 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`. @@ -27830,7 +27814,7 @@ components: expiresAt: type: string format: date-time - description: ISO 8601 timestamp, in UTC, after which the `sessionToken` beside it is refused with `CARE_SESSION_EXPIRED`. Present exactly when `sessionToken` is present. Read it rather than assuming a duration. + description: '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.' example: '2026-09-12T14:25:00Z' CareError: type: object @@ -27860,7 +27844,7 @@ components: | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `customerId` that is not a `Customer` id, or a chat that cannot be verified at all. | | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused at the content boundary and no turn ran. `reason` carries the sentence to show them. | | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified — that is signalled in a `200` body. | - | `CARE_SESSION_EXPIRED` | 401 | The chat's verification is still current, so it requires `X-Ls-Care-Session`, and the header was missing; or the token presented was malformed or past its expiry. Retry the message with no `X-Ls-Care-Session` header. If that is refused too, the token for a live verification has been lost: close the chat and send the message to the cardholder as `Customer:`, which opens a fresh chat and sends a new code. Once the chat's own verification has aged past the session lifetime this error stops: the header is no longer required, and the next message sends a fresh code instead of being refused. | + | `CARE_SESSION_EXPIRED` | 401 | The `X-Ls-Care-Session` token presented is malformed, or not a care session token at all. A missing or expired token is never this error: the message verifies the cardholder again inside a `200` instead. Rarely, also returned when a verification lands on the chat at the same moment a message without a live token takes it away; send the message again. | | `FORBIDDEN` | 403 | Embedded care is not enabled for this platform. | | `CARE_SESSION_PLATFORM_MISMATCH` | 403 | The session token was minted for a different platform. Stop; retrying will not help. | | `NOT_FOUND` | 404 | No such chat for this platform. A chat belonging to another platform, a chat that never existed, a malformed chat id, and an `X-Ls-Care-Session` token minted for a different chat or a different cardholder are one answer. | @@ -27898,7 +27882,7 @@ components: reason: type: string description: Human-readable explanation, for a log or a support ticket. Not for branching, and not always safe to show a cardholder verbatim — the exception is `CARE_MESSAGE_REJECTED`, whose `reason` is written to be shown. - example: The care session has expired. Verify the cardholder again. + example: The care session token is not valid. WebhookType: type: string enum: diff --git a/openapi/components/schemas/care/CareChatGreeting.yaml b/openapi/components/schemas/care/CareChatGreeting.yaml deleted file mode 100644 index 332744acb..000000000 --- a/openapi/components/schemas/care/CareChatGreeting.yaml +++ /dev/null @@ -1,25 +0,0 @@ -type: object -description: >- - The agent's opening line, recorded as the chat's first message. Returned - only by the call that opened the chat, never on a resume: a greeting - arriving mid-conversation reads as the agent introducing itself a second - time. Absent when the agent produced no opening line. -required: - - id - - content - - createdAt -properties: - id: - type: string - description: Id of the recorded greeting message. - example: CareMessage:019542f5-b3e7-1d02-0000-000000000031 - content: - type: string - description: >- - The greeting text to render in the transcript, in the chat's locale. - example: Hi, I'm the support assistant. What can I help you with today? - createdAt: - type: string - format: date-time - description: ISO 8601 timestamp, in UTC, of when the greeting was recorded. - example: '2026-09-12T14:10:00.123456Z' diff --git a/openapi/components/schemas/care/CareChatMessageResponse.yaml b/openapi/components/schemas/care/CareChatMessageResponse.yaml index 7ce98181e..edd3505ce 100644 --- a/openapi/components/schemas/care/CareChatMessageResponse.yaml +++ b/openapi/components/schemas/care/CareChatMessageResponse.yaml @@ -2,11 +2,12 @@ type: object description: >- The result of one call to the message route. Which fields are present depends on what the call did: a turn adds `id`, `displayMessage`, `reply` - and `replayed`; the call that opened the chat adds `greeting`; an - outstanding verification adds `destinationMasked`, `alreadySent` and, - where the service could be read, `codeLength`; and the turn in which the + and `replayed`; the call that opened the chat adds `reply` alone, carrying + the agent's opening line; an outstanding verification adds + `destinationMasked`, `alreadySent` and, where the service could be read, + `codeLength`; and the turn in which the cardholder sent their verification code adds `sessionToken` and - `expiresAt`. + `expiresAt`. Render `displayMessage` if present, then `reply` if present. required: - chatId - verificationStatus @@ -30,10 +31,10 @@ properties: false, so a client reads a field rather than an absence. When true, `destinationMasked` and `alreadySent` are present, and the cardholder completes verification by sending the code as an ordinary message. A - verification that - is outstanding is never reported as a `401`: on this surface a `401` - means the platform credential is wrong, or a chat whose verification is - still current was driven without its session token. + verification that is outstanding, or one taken away because a message + arrived without a live session token, is never reported as a `401`: on + this surface a `401` means the platform credential is wrong, or the + session token presented is malformed. Read this field rather than `verificationStatus`. A chat whose @@ -68,11 +69,14 @@ properties: reply: type: string description: >- - The agent's answer to this message, to render as the assistant's turn. - Present only when a turn ran. 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. - example: I can help with that. To look at your account I've texted you a code — what does it say? + 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`, consumes none and bills + nothing, even though it carries a `reply`. 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. + example: I can help with that. To look at your account I've texted you a code. What does it say? replayed: type: boolean description: >- @@ -82,8 +86,6 @@ properties: under the original key is the only way to recover a reply that was produced but never delivered. example: false - greeting: - $ref: ./CareChatGreeting.yaml destinationMasked: type: string description: >- @@ -120,9 +122,9 @@ properties: ordinary message. The agent checks it with the SMS provider, and the chat comes out of that turn able to reach the account. This field is how the partner is handed the credential that turn earned, and it is the - only place a session token is ever issued. Without it you would hold a - chat that is privileged and unusable at once, because every later - message on it is refused with `CARE_SESSION_EXPIRED`. + only place a session token is ever issued. A later message sent without + it takes the verification away again and asks the cardholder for a new + code. Send it as the `X-Ls-Care-Session` header from the next call onwards. @@ -139,7 +141,8 @@ properties: type: string format: date-time description: >- - ISO 8601 timestamp, in UTC, after which the `sessionToken` beside it is - refused with `CARE_SESSION_EXPIRED`. Present exactly when + 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. example: '2026-09-12T14:25:00Z' diff --git a/openapi/components/schemas/care/CareError.yaml b/openapi/components/schemas/care/CareError.yaml index ca66c33cd..eff25e8b6 100644 --- a/openapi/components/schemas/care/CareError.yaml +++ b/openapi/components/schemas/care/CareError.yaml @@ -25,7 +25,7 @@ properties: | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `customerId` that is not a `Customer` id, or a chat that cannot be verified at all. | | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused at the content boundary and no turn ran. `reason` carries the sentence to show them. | | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified — that is signalled in a `200` body. | - | `CARE_SESSION_EXPIRED` | 401 | The chat's verification is still current, so it requires `X-Ls-Care-Session`, and the header was missing; or the token presented was malformed or past its expiry. Retry the message with no `X-Ls-Care-Session` header. If that is refused too, the token for a live verification has been lost: close the chat and send the message to the cardholder as `Customer:`, which opens a fresh chat and sends a new code. Once the chat's own verification has aged past the session lifetime this error stops: the header is no longer required, and the next message sends a fresh code instead of being refused. | + | `CARE_SESSION_EXPIRED` | 401 | The `X-Ls-Care-Session` token presented is malformed, or not a care session token at all. A missing or expired token is never this error: the message verifies the cardholder again inside a `200` instead. Rarely, also returned when a verification lands on the chat at the same moment a message without a live token takes it away; send the message again. | | `FORBIDDEN` | 403 | Embedded care is not enabled for this platform. | | `CARE_SESSION_PLATFORM_MISMATCH` | 403 | The session token was minted for a different platform. Stop; retrying will not help. | | `NOT_FOUND` | 404 | No such chat for this platform. A chat belonging to another platform, a chat that never existed, a malformed chat id, and an `X-Ls-Care-Session` token minted for a different chat or a different cardholder are one answer. | @@ -67,4 +67,4 @@ properties: branching, and not always safe to show a cardholder verbatim — the exception is `CARE_MESSAGE_REJECTED`, whose `reason` is written to be shown. - example: The care session has expired. Verify the cardholder again. + example: The care session token is not valid. diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 6f35111ff..31e1dfbaf 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -111,10 +111,11 @@ tags: 1. **Open the chat.** `POST /care/chats/{chatId}/messages` with an empty `message`, addressed to the cardholder as `Customer:`. Returns - the chat's id and the agent's `greeting`, which is what a chat window - renders before anybody has typed. Sends no SMS, runs no turn, bills - nothing, and is safe to call again: a widget that reloaded gets the - same chat back, this time without a greeting. + the chat's id and the agent's opening line in `reply`, which is what + a chat window renders before anybody has typed. Sends no SMS, runs no + turn, needs no `Idempotency-Key`, bills nothing, and is safe to call + again: a widget that reloaded gets the same chat back, this time + without a `reply`. 2. **Send the cardholder's first message.** The same route with their text and an `Idempotency-Key` header. One turn runs synchronously and the agent's answer comes back in `reply`. On a chat that cannot yet @@ -135,6 +136,9 @@ tags: one.** The agent sends a new code to the number on file and says so in its `reply`. There is no separate route for it. + **Render `displayMessage` if present, then `reply` if present.** That one + rule covers an opening, a resume and a turn. + Step 1 is optional. A message addressed to a `Customer:` id opens that cardholder's chat if they have none, so a partner whose cardholder has just typed something spends one call rather than two. @@ -143,24 +147,21 @@ tags: reported inside a `200`, because the first message from every new cardholder is unverified and a `401` there would be indistinguishable from a bad credential. On these endpoints a `401` means the platform's - Grid credential is wrong, or a chat whose verification is still current - was driven without its session token. + Grid credential is wrong, or the session token presented is malformed. - **Verification expires, and expiry is recoverable in the chat.** 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 also how long a session token lives. Past that window the chat - stops requiring the header: a message sent without one gets a fresh - code, and the agent asks the cardholder to send it back inside the - conversation, which hands the partner the next session token. Sending - the dead token anyway is still a `401 CARE_SESSION_EXPIRED`, so the - handling is to discard the token on that error and resend the message - with no `X-Ls-Care-Session` header rather than to tear the chat down. - If that resend is refused too, the chat's verification is still current - and its token has been lost. Close the chat with - `DELETE /care/chats/{chatId}`, which needs no token, and send the - message to the cardholder as `Customer:`: that opens a fresh chat - and sends a new code. + **Verification expires, and a lost session is recoverable in the + chat.** 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 also how long a session token lives. Past + that window a message gets a fresh code, and the agent asks the + cardholder to send it back inside the conversation, which hands the + partner the next session token. A 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 taken away before the turn runs, a new code is sent, and + the agent asks for it. So a client that lost its token, or held one too + long, simply keeps sending messages. Only a token that is malformed, or + that was minted for another platform, chat or cardholder, is refused. Two things an integration has to get right: @@ -173,7 +174,8 @@ tags: endpoint hands it back, there is no revocation, and it is short-lived by design. Hold it for the life of the cardholder's session. It arrives on the message response to the turn in which the cardholder - sent their code, and on no other. + sent their code, and on no other. Losing it costs the cardholder a new + code, never access to the chat. Some things are absent by construction, so no integration can come to depend on them: nothing returns a transcript or an earlier turn's reply diff --git a/openapi/paths/care/care_chats_{chatId}_messages.yaml b/openapi/paths/care/care_chats_{chatId}_messages.yaml index 3eebf9872..815bbf168 100644 --- a/openapi/paths/care/care_chats_{chatId}_messages.yaml +++ b/openapi/paths/care/care_chats_{chatId}_messages.yaml @@ -30,12 +30,13 @@ post: may be sent again and will run a fresh turn. **An empty or absent `message` opens the chat and runs nothing.** On a - cardholder with no open chat it opens one and answers with `greeting`, 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 `greeting` does not, because - a greeting arriving mid-conversation reads as the agent introducing itself - a second time. Either way no code is sent, no `Idempotency-Key` is needed + 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`, `displayMessage` and `replayed` are absent on + both, because nothing was typed and no turn ran. Either way no code is sent, no `Idempotency-Key` is needed or consumed, and nothing is billed — a widget in a reconnect loop may call it as often as it likes. @@ -49,9 +50,10 @@ post: asks the cardholder for the code. Later messages on the same chat find the code already outstanding and send nothing, reporting `alreadySent: true`. - **Render `displayMessage`, not your own copy of the input.** Grid redacts - the cardholder's text on the way in — a card number they pasted comes back - masked — and `displayMessage` is that redacted echo. A UI that echoes its + **Render `displayMessage` if present, then `reply` if present.** That one + rule covers every call: an opening, a resume and a turn. Grid redacts the + cardholder's text on the way in, so a card number they pasted comes back + masked, and `displayMessage` is that redacted echo. A UI that echoes its own input box instead has put an unmasked PAN in the transcript, on the screen and in whatever logs the transcript reaches. @@ -61,17 +63,15 @@ post: that turn able to reach the account. 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, or you will hold a chat that is privileged and - unusable at once. A code that does not match is answered in `reply`, not + 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, every message must carry the - session token in the `X-Ls-Care-Session` header. Without it the call is - refused with `CARE_SESSION_EXPIRED`. + 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 @@ -85,20 +85,25 @@ post: `codeLength`, and the agent's own `reply` asks the cardholder to send the code. The turn still runs and still answers. - Sending the expired token anyway is still a `401 CARE_SESSION_EXPIRED`. - One recovery covers a token that was lost, one that was never valid and - one that has expired: retry the message with no `X-Ls-Care-Session` - header. On a chat whose verification has gone stale that call succeeds, - sends a fresh code and has the agent ask for it, and the turn in which the - cardholder sends it returns the next `sessionToken`. On a chat whose - verification is still current it is refused with `CARE_SESSION_EXPIRED` - again, which means the token for a live verification has been lost: close - the chat with `DELETE /care/chats/{chatId}`, which needs no token, and - send the message to the cardholder as `Customer:`, which opens a - fresh chat and sends a new code. 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. + **A lost or expired session token verifies again rather than being + refused.** A 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 chat's verification is taken away 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 body + carries `verificationRequired: true` with `destinationMasked`, + `alreadySent` and `codeLength`, the agent asks the cardholder for the + code, 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 describes the chat as of the moment the turn started, `verificationStatus` included. The agent can verify a cardholder @@ -134,13 +139,15 @@ post: required: false description: >- The `sessionToken` this route returned on the turn in which the - cardholder sent their verification code. Required on every message while the chat's verification - is current, and refused with `CARE_SESSION_EXPIRED` when it is missing - there. Not required before the cardholder has verified, nor once the - chat's verification has aged past the session lifetime; but a token - that is sent is always checked, so a malformed or expired one is - refused with `CARE_SESSION_EXPIRED` whatever state the chat is in, and - one minted for another chat or another cardholder is `NOT_FOUND`. An + cardholder sent their verification code. Send it on every message + while the chat's verification is current. A message without it, or + with one past its expiry, is not refused: the chat's verification is + taken away before the turn runs and a new code is sent, so the + cardholder verifies again in the conversation. A token that is sent is + always checked for what it names: a malformed one is refused with + `CARE_SESSION_EXPIRED`, one minted for another chat or another + cardholder is `NOT_FOUND`, and one minted for another platform is + `CARE_SESSION_PLATFORM_MISMATCH`, whether or not it has expired. An opening never reads the header at all, because a widget that reloaded has lost its token and that is the case the opening exists for. schema: @@ -154,7 +161,7 @@ post: $ref: ../../components/schemas/care/CareChatMessageRequest.yaml examples: openTheChat: - summary: Open or resume the cardholder's chat, saying nothing + summary: Open or resume the cardholder's chat; an opening returns the agent's opening line as reply value: message: '' firstMessage: @@ -194,12 +201,13 @@ post: '401': description: >- `UNAUTHORIZED` when the platform's Grid credential is missing or - wrong. `CARE_SESSION_EXPIRED` when the chat's verification is still - current and the `X-Ls-Care-Session` header is missing, or when the - token presented is malformed or past its expiry. An unverified - cardholder is never a `401`; that is a `200` carrying - `verificationRequired`. Neither is a chat whose verification has gone - stale, which is asked for a code again inside a `200`. + wrong. `CARE_SESSION_EXPIRED` when the `X-Ls-Care-Session` token + presented is malformed. A missing or expired session token is never a + `401`: it 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. content: application/json: schema: From 1c1ba1e068792b182e0bc8f646b31f01f90cd37e Mon Sep 17 00:00:00 2001 From: kphurley7 Date: Wed, 16 Sep 2026 17:50:03 -0500 Subject: [PATCH 06/11] docs(care): close the message body schema and name the right path id CareChatMessageRequest said unknown fields are rejected but did not declare additionalProperties false, so the contract allowed them; the server model forbids extra fields, and the schema now says so. The CareError INVALID_INPUT row named a customerId field that does not exist on this surface; it now names the chatId path id beginning Customer:. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015njn7Fen4mg9NDX7AZQRTj --- mintlify/openapi.yaml | 5 +++-- openapi.yaml | 5 +++-- openapi/components/schemas/care/CareChatMessageRequest.yaml | 5 +++-- openapi/components/schemas/care/CareError.yaml | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 7b369c734..752e5e37d 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -29145,7 +29145,8 @@ components: example: 2VcUIonJeVQzFoBuC7LdFT0dRe4 CareChatMessageRequest: type: object - description: 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`. + description: 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`. + additionalProperties: false properties: message: type: string @@ -29287,7 +29288,7 @@ components: | Error Code | HTTP | Description | |------------|------|-------------| - | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `customerId` that is not a `Customer` id, or a chat that cannot be verified at all. | + | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `chatId` path id beginning `Customer:` that is not well formed, or a chat that cannot be verified at all. | | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused at the content boundary and no turn ran. `reason` carries the sentence to show them. | | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified — that is signalled in a `200` body. | | `CARE_SESSION_EXPIRED` | 401 | The `X-Ls-Care-Session` token presented is malformed, or not a care session token at all. A missing or expired token is never this error: the message verifies the cardholder again inside a `200` instead. Rarely, also returned when a verification lands on the chat at the same moment a message without a live token takes it away; send the message again. | diff --git a/openapi.yaml b/openapi.yaml index 7b369c734..752e5e37d 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -29145,7 +29145,8 @@ components: example: 2VcUIonJeVQzFoBuC7LdFT0dRe4 CareChatMessageRequest: type: object - description: 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`. + description: 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`. + additionalProperties: false properties: message: type: string @@ -29287,7 +29288,7 @@ components: | Error Code | HTTP | Description | |------------|------|-------------| - | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `customerId` that is not a `Customer` id, or a chat that cannot be verified at all. | + | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `chatId` path id beginning `Customer:` that is not well formed, or a chat that cannot be verified at all. | | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused at the content boundary and no turn ran. `reason` carries the sentence to show them. | | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified — that is signalled in a `200` body. | | `CARE_SESSION_EXPIRED` | 401 | The `X-Ls-Care-Session` token presented is malformed, or not a care session token at all. A missing or expired token is never this error: the message verifies the cardholder again inside a `200` instead. Rarely, also returned when a verification lands on the chat at the same moment a message without a live token takes it away; send the message again. | diff --git a/openapi/components/schemas/care/CareChatMessageRequest.yaml b/openapi/components/schemas/care/CareChatMessageRequest.yaml index 8c54a33dc..a67b93d54 100644 --- a/openapi/components/schemas/care/CareChatMessageRequest.yaml +++ b/openapi/components/schemas/care/CareChatMessageRequest.yaml @@ -1,8 +1,9 @@ type: object description: >- - One turn of the cardholder's conversation, or — with `message` empty or - absent — the call that opens or resumes the chat without saying anything. + 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`. +additionalProperties: false properties: message: type: string diff --git a/openapi/components/schemas/care/CareError.yaml b/openapi/components/schemas/care/CareError.yaml index eff25e8b6..e5fda869c 100644 --- a/openapi/components/schemas/care/CareError.yaml +++ b/openapi/components/schemas/care/CareError.yaml @@ -22,7 +22,7 @@ properties: | Error Code | HTTP | Description | |------------|------|-------------| - | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `customerId` that is not a `Customer` id, or a chat that cannot be verified at all. | + | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `chatId` path id beginning `Customer:` that is not well formed, or a chat that cannot be verified at all. | | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused at the content boundary and no turn ran. `reason` carries the sentence to show them. | | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified — that is signalled in a `200` body. | | `CARE_SESSION_EXPIRED` | 401 | The `X-Ls-Care-Session` token presented is malformed, or not a care session token at all. A missing or expired token is never this error: the message verifies the cardholder again inside a `200` instead. Rarely, also returned when a verification lands on the chat at the same moment a message without a live token takes it away; send the message again. | From 8b70dbcdf28d11196242f0e470d3a9b657c13068 Mon Sep 17 00:00:00 2001 From: kphurley7 Date: Wed, 16 Sep 2026 20:52:09 -0500 Subject: [PATCH 07/11] docs(care): chatEnded, opening an ended chat, and replays without a session The message response gains chatEnded, required and present on every body: the chat's state once the call finished, true when no further message will be accepted, including on the turn where the agent ends the chat (closing line in reply) and on a replay of it. The tag overview tells clients to stop sending to a chat when it is true. An opening addressed to a CareChat: that has ended now answers 409 CARE_CHAT_ENDED, with addressing the cardholder as Customer: as the recovery; the reconnect-loop wording says so. A replay of a completed turn is still answered after the chat ended. A replay under the same Idempotency-Key returns the recorded reply but never a sessionToken; the sessionToken and replayed fields, the message route and the overview say so, with the recovery for a lost token: the next message without one re-verifies the cardholder. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015njn7Fen4mg9NDX7AZQRTj --- mintlify/openapi.yaml | 50 +++++++++++++++---- openapi.yaml | 50 +++++++++++++++---- .../schemas/care/CareChatMessageResponse.yaml | 24 ++++++++- .../components/schemas/care/CareError.yaml | 2 +- openapi/openapi.yaml | 11 +++- .../care/care_chats_{chatId}_messages.yaml | 37 ++++++++++---- 6 files changed, 140 insertions(+), 34 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 752e5e37d..1e7f40908 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -94,7 +94,12 @@ tags: does not match is answered in `reply`, and the cardholder can simply send it again. 4. **Close the chat** with `DELETE /care/chats/{chatId}` when the - cardholder is done. + cardholder is done. Every response also carries `chatEnded`, and the + agent can end the chat itself: when `chatEnded` is true, the closing + line is in `reply` and the chat takes no further messages, so stop + sending to it. An opening addressed to that `CareChat:` answers + `409 CARE_CHAT_ENDED`; address the cardholder as `Customer:` + to open a new chat. **A cardholder whose code never arrived asks the agent for another one.** The agent sends a new code to the number on file and says so in @@ -144,7 +149,9 @@ tags: Some things are absent by construction, so no integration can come to depend on them: nothing returns a transcript or an earlier turn's reply (retrying under the original `Idempotency-Key` is how an answer that was - produced but not delivered is recovered), nothing sets a verification + produced but not delivered is recovered, and such a replay never carries + a `sessionToken`; a lost token is recovered by the cardholder's next + message verifying them again), nothing sets a verification state, and nothing changes the number a code is sent to. Errors from these endpoints carry `code` and `reason` only — see the @@ -11086,8 +11093,13 @@ paths: to recover an answer: nothing on this surface reads a transcript, so a reply that was produced but not delivered is reachable only by retrying under the same key, which returns the recorded answer with - `replayed: true`. A turn that failed does not pin its key — the same key - may be sent again and will run a fresh turn. + `replayed: true`. A replay returns the recorded reply but never a + `sessionToken`, even if the original response carried one. If the + response carrying the token was lost, send the cardholder's next message + without a token: the verification is cancelled, a new code is sent, and + the agent asks for it. A replay is answered even after the chat has ended. + 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 nothing.** On a cardholder with no open chat it opens one, and `reply` carries the agent's @@ -11096,9 +11108,20 @@ paths: 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`, `displayMessage` and `replayed` are absent on - both, because nothing was typed and no turn ran. Either way no code is sent, no `Idempotency-Key` is needed - or consumed, and nothing is billed — a widget in a reconnect loop may call - it as often as it likes. + both, because nothing was typed and no turn ran. Either way no code is + sent, no `Idempotency-Key` is needed or consumed, and nothing is billed, + 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:` that has ended: + it answers `409 CARE_CHAT_ENDED`, and the recovery is to address the + cardholder as `Customer:`, 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 is signalled in this `200` body, never as a `401`.** Every new cardholder's first message is unverified, so a `401` there would make @@ -11248,7 +11271,7 @@ paths: schema: $ref: '#/components/schemas/CareError' '409': - description: '`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 carrying text is sent to a chat that has been closed; open a new one by sending a message addressed to the cardholder as `Customer:`, which finds no open chat and opens a fresh one.' + description: '`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 already completed; open a new one by sending a message addressed to the cardholder as `Customer:`, which finds no open chat and opens a fresh one.' content: application/json: schema: @@ -29203,6 +29226,7 @@ components: - chatId - verificationStatus - verificationRequired + - chatEnded properties: chatId: type: string @@ -29210,6 +29234,10 @@ components: example: CareChat:019542f5-b3e7-1d02-0000-000000000030 verificationStatus: $ref: '#/components/schemas/CareChatVerificationStatus' + chatEnded: + type: boolean + description: '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:`, which opens a new chat.' + example: false verificationRequired: type: boolean description: |- @@ -29233,7 +29261,7 @@ components: example: I can help with that. To look at your account I've texted you a code. What does it say? replayed: type: boolean - description: Whether this answer came from the record of an earlier call under the same `Idempotency-Key` rather than from a turn run now. Present only when a turn ran. There is no route that reads a transcript, so a retry under the original key is the only way to recover a reply that was produced but never delivered. + description: Whether this answer came from the record of an earlier call under the same `Idempotency-Key` rather than from a turn run now. Present only when a turn ran. There is no route that reads a transcript, so a retry under the original key is the only way to recover a reply that was produced but never delivered. A replay never carries a `sessionToken`, even if the original response did; if that response was lost, the cardholder's next message without a token verifies them again. example: false destinationMasked: type: string @@ -29257,6 +29285,8 @@ components: Send it as the `X-Ls-Care-Session` header from the next call onwards. It is returned once and never re-readable, there is no revocation, 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` returns the recorded reply but never a `sessionToken`, even if the original response carried one. If the response carrying the token was lost, send the cardholder's next message without a token: the verification is cancelled, a new code is sent, and the agent asks for it. example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl expiresAt: type: string @@ -29297,7 +29327,7 @@ components: | `NOT_FOUND` | 404 | No such chat for this platform. A chat belonging to another platform, a chat that never existed, a malformed chat id, and an `X-Ls-Care-Session` token minted for a different chat or a different cardholder are one answer. | | `CUSTOMER_NOT_FOUND` | 404 | No such cardholder for this platform. | | `CONFLICT` | 409 | The `Idempotency-Key` was already used for a different message or a different chat — use a new key — or the chat has no cardholder bound to it. | - | `CARE_CHAT_ENDED` | 409 | The chat has been closed. Open a new one by sending an empty message to the cardholder. | + | `CARE_CHAT_ENDED` | 409 | The chat has ended, whether the agent ended it or it was closed, and it accepts no further messages or openings; a replay of a turn that already completed is still answered. Open a new one by sending an empty message to the cardholder as `Customer:`. | | `CARE_NOT_CONFIGURED_FOR_PLATFORM` | 422 | The platform is entitled to embedded care but has no care programme configured. | | `CARE_BUDGET_EXCEEDED` | 429 | This chat has reached its limit. It is not replaced by a new one: a replacement would be the limit cleared by asking again. | | `RATE_LIMITED` | 429 | The platform is calling this endpoint too often. Back off for the interval in the `Retry-After` response header. | diff --git a/openapi.yaml b/openapi.yaml index 752e5e37d..1e7f40908 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -94,7 +94,12 @@ tags: does not match is answered in `reply`, and the cardholder can simply send it again. 4. **Close the chat** with `DELETE /care/chats/{chatId}` when the - cardholder is done. + cardholder is done. Every response also carries `chatEnded`, and the + agent can end the chat itself: when `chatEnded` is true, the closing + line is in `reply` and the chat takes no further messages, so stop + sending to it. An opening addressed to that `CareChat:` answers + `409 CARE_CHAT_ENDED`; address the cardholder as `Customer:` + to open a new chat. **A cardholder whose code never arrived asks the agent for another one.** The agent sends a new code to the number on file and says so in @@ -144,7 +149,9 @@ tags: Some things are absent by construction, so no integration can come to depend on them: nothing returns a transcript or an earlier turn's reply (retrying under the original `Idempotency-Key` is how an answer that was - produced but not delivered is recovered), nothing sets a verification + produced but not delivered is recovered, and such a replay never carries + a `sessionToken`; a lost token is recovered by the cardholder's next + message verifying them again), nothing sets a verification state, and nothing changes the number a code is sent to. Errors from these endpoints carry `code` and `reason` only — see the @@ -11086,8 +11093,13 @@ paths: to recover an answer: nothing on this surface reads a transcript, so a reply that was produced but not delivered is reachable only by retrying under the same key, which returns the recorded answer with - `replayed: true`. A turn that failed does not pin its key — the same key - may be sent again and will run a fresh turn. + `replayed: true`. A replay returns the recorded reply but never a + `sessionToken`, even if the original response carried one. If the + response carrying the token was lost, send the cardholder's next message + without a token: the verification is cancelled, a new code is sent, and + the agent asks for it. A replay is answered even after the chat has ended. + 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 nothing.** On a cardholder with no open chat it opens one, and `reply` carries the agent's @@ -11096,9 +11108,20 @@ paths: 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`, `displayMessage` and `replayed` are absent on - both, because nothing was typed and no turn ran. Either way no code is sent, no `Idempotency-Key` is needed - or consumed, and nothing is billed — a widget in a reconnect loop may call - it as often as it likes. + both, because nothing was typed and no turn ran. Either way no code is + sent, no `Idempotency-Key` is needed or consumed, and nothing is billed, + 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:` that has ended: + it answers `409 CARE_CHAT_ENDED`, and the recovery is to address the + cardholder as `Customer:`, 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 is signalled in this `200` body, never as a `401`.** Every new cardholder's first message is unverified, so a `401` there would make @@ -11248,7 +11271,7 @@ paths: schema: $ref: '#/components/schemas/CareError' '409': - description: '`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 carrying text is sent to a chat that has been closed; open a new one by sending a message addressed to the cardholder as `Customer:`, which finds no open chat and opens a fresh one.' + description: '`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 already completed; open a new one by sending a message addressed to the cardholder as `Customer:`, which finds no open chat and opens a fresh one.' content: application/json: schema: @@ -29203,6 +29226,7 @@ components: - chatId - verificationStatus - verificationRequired + - chatEnded properties: chatId: type: string @@ -29210,6 +29234,10 @@ components: example: CareChat:019542f5-b3e7-1d02-0000-000000000030 verificationStatus: $ref: '#/components/schemas/CareChatVerificationStatus' + chatEnded: + type: boolean + description: '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:`, which opens a new chat.' + example: false verificationRequired: type: boolean description: |- @@ -29233,7 +29261,7 @@ components: example: I can help with that. To look at your account I've texted you a code. What does it say? replayed: type: boolean - description: Whether this answer came from the record of an earlier call under the same `Idempotency-Key` rather than from a turn run now. Present only when a turn ran. There is no route that reads a transcript, so a retry under the original key is the only way to recover a reply that was produced but never delivered. + description: Whether this answer came from the record of an earlier call under the same `Idempotency-Key` rather than from a turn run now. Present only when a turn ran. There is no route that reads a transcript, so a retry under the original key is the only way to recover a reply that was produced but never delivered. A replay never carries a `sessionToken`, even if the original response did; if that response was lost, the cardholder's next message without a token verifies them again. example: false destinationMasked: type: string @@ -29257,6 +29285,8 @@ components: Send it as the `X-Ls-Care-Session` header from the next call onwards. It is returned once and never re-readable, there is no revocation, 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` returns the recorded reply but never a `sessionToken`, even if the original response carried one. If the response carrying the token was lost, send the cardholder's next message without a token: the verification is cancelled, a new code is sent, and the agent asks for it. example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl expiresAt: type: string @@ -29297,7 +29327,7 @@ components: | `NOT_FOUND` | 404 | No such chat for this platform. A chat belonging to another platform, a chat that never existed, a malformed chat id, and an `X-Ls-Care-Session` token minted for a different chat or a different cardholder are one answer. | | `CUSTOMER_NOT_FOUND` | 404 | No such cardholder for this platform. | | `CONFLICT` | 409 | The `Idempotency-Key` was already used for a different message or a different chat — use a new key — or the chat has no cardholder bound to it. | - | `CARE_CHAT_ENDED` | 409 | The chat has been closed. Open a new one by sending an empty message to the cardholder. | + | `CARE_CHAT_ENDED` | 409 | The chat has ended, whether the agent ended it or it was closed, and it accepts no further messages or openings; a replay of a turn that already completed is still answered. Open a new one by sending an empty message to the cardholder as `Customer:`. | | `CARE_NOT_CONFIGURED_FOR_PLATFORM` | 422 | The platform is entitled to embedded care but has no care programme configured. | | `CARE_BUDGET_EXCEEDED` | 429 | This chat has reached its limit. It is not replaced by a new one: a replacement would be the limit cleared by asking again. | | `RATE_LIMITED` | 429 | The platform is calling this endpoint too often. Back off for the interval in the `Retry-After` response header. | diff --git a/openapi/components/schemas/care/CareChatMessageResponse.yaml b/openapi/components/schemas/care/CareChatMessageResponse.yaml index edd3505ce..507cb21d3 100644 --- a/openapi/components/schemas/care/CareChatMessageResponse.yaml +++ b/openapi/components/schemas/care/CareChatMessageResponse.yaml @@ -12,6 +12,7 @@ required: - chatId - verificationStatus - verificationRequired + - chatEnded properties: chatId: type: string @@ -23,6 +24,18 @@ properties: example: CareChat:019542f5-b3e7-1d02-0000-000000000030 verificationStatus: $ref: ./CareChatVerificationStatus.yaml + chatEnded: + type: boolean + description: >- + 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:`, which opens a new chat. + example: false verificationRequired: type: boolean description: >- @@ -84,7 +97,9 @@ properties: same `Idempotency-Key` rather than from a turn run now. Present only when a turn ran. There is no route that reads a transcript, so a retry under the original key is the only way to recover a reply that was - produced but never delivered. + produced but never delivered. A replay never carries a `sessionToken`, + even if the original response did; if that response was lost, the + cardholder's next message without a token verifies them again. example: false destinationMasked: type: string @@ -136,6 +151,13 @@ properties: 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` returns the recorded reply + but never a `sessionToken`, even if the original response carried one. + If the response carrying the token was lost, send the cardholder's next + message without a token: the verification is cancelled, a new code is + sent, and the agent asks for it. example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl expiresAt: type: string diff --git a/openapi/components/schemas/care/CareError.yaml b/openapi/components/schemas/care/CareError.yaml index e5fda869c..8e8a55f99 100644 --- a/openapi/components/schemas/care/CareError.yaml +++ b/openapi/components/schemas/care/CareError.yaml @@ -31,7 +31,7 @@ properties: | `NOT_FOUND` | 404 | No such chat for this platform. A chat belonging to another platform, a chat that never existed, a malformed chat id, and an `X-Ls-Care-Session` token minted for a different chat or a different cardholder are one answer. | | `CUSTOMER_NOT_FOUND` | 404 | No such cardholder for this platform. | | `CONFLICT` | 409 | The `Idempotency-Key` was already used for a different message or a different chat — use a new key — or the chat has no cardholder bound to it. | - | `CARE_CHAT_ENDED` | 409 | The chat has been closed. Open a new one by sending an empty message to the cardholder. | + | `CARE_CHAT_ENDED` | 409 | The chat has ended, whether the agent ended it or it was closed, and it accepts no further messages or openings; a replay of a turn that already completed is still answered. Open a new one by sending an empty message to the cardholder as `Customer:`. | | `CARE_NOT_CONFIGURED_FOR_PLATFORM` | 422 | The platform is entitled to embedded care but has no care programme configured. | | `CARE_BUDGET_EXCEEDED` | 429 | This chat has reached its limit. It is not replaced by a new one: a replacement would be the limit cleared by asking again. | | `RATE_LIMITED` | 429 | The platform is calling this endpoint too often. Back off for the interval in the `Retry-After` response header. | diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 79ecc733a..2685b6755 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -130,7 +130,12 @@ tags: does not match is answered in `reply`, and the cardholder can simply send it again. 4. **Close the chat** with `DELETE /care/chats/{chatId}` when the - cardholder is done. + cardholder is done. Every response also carries `chatEnded`, and the + agent can end the chat itself: when `chatEnded` is true, the closing + line is in `reply` and the chat takes no further messages, so stop + sending to it. An opening addressed to that `CareChat:` answers + `409 CARE_CHAT_ENDED`; address the cardholder as `Customer:` + to open a new chat. **A cardholder whose code never arrived asks the agent for another one.** The agent sends a new code to the number on file and says so in @@ -180,7 +185,9 @@ tags: Some things are absent by construction, so no integration can come to depend on them: nothing returns a transcript or an earlier turn's reply (retrying under the original `Idempotency-Key` is how an answer that was - produced but not delivered is recovered), nothing sets a verification + produced but not delivered is recovered, and such a replay never carries + a `sessionToken`; a lost token is recovered by the cardholder's next + message verifying them again), nothing sets a verification state, and nothing changes the number a code is sent to. Errors from these endpoints carry `code` and `reason` only — see the diff --git a/openapi/paths/care/care_chats_{chatId}_messages.yaml b/openapi/paths/care/care_chats_{chatId}_messages.yaml index 815bbf168..d04cbef63 100644 --- a/openapi/paths/care/care_chats_{chatId}_messages.yaml +++ b/openapi/paths/care/care_chats_{chatId}_messages.yaml @@ -26,8 +26,13 @@ post: to recover an answer: nothing on this surface reads a transcript, so a reply that was produced but not delivered is reachable only by retrying under the same key, which returns the recorded answer with - `replayed: true`. A turn that failed does not pin its key — the same key - may be sent again and will run a fresh turn. + `replayed: true`. A replay returns the recorded reply but never a + `sessionToken`, even if the original response carried one. If the + response carrying the token was lost, send the cardholder's next message + without a token: the verification is cancelled, a new code is sent, and + the agent asks for it. A replay is answered even after the chat has ended. + 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 nothing.** On a cardholder with no open chat it opens one, and `reply` carries the agent's @@ -36,9 +41,20 @@ post: 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`, `displayMessage` and `replayed` are absent on - both, because nothing was typed and no turn ran. Either way no code is sent, no `Idempotency-Key` is needed - or consumed, and nothing is billed — a widget in a reconnect loop may call - it as often as it likes. + both, because nothing was typed and no turn ran. Either way no code is + sent, no `Idempotency-Key` is needed or consumed, and nothing is billed, + 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:` that has ended: + it answers `409 CARE_CHAT_ENDED`, and the recovery is to address the + cardholder as `Customer:`, 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 is signalled in this `200` body, never as a `401`.** Every new cardholder's first message is unverified, so a `401` there would make @@ -237,11 +253,12 @@ post: '409': description: >- `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 carrying text - is sent to a chat that has been closed; open a new one by sending a - message addressed to the cardholder as `Customer:`, which finds - no open chat and opens a fresh one. + 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 already completed; open a new one by sending a message + addressed to the cardholder as `Customer:`, which finds no open + chat and opens a fresh one. content: application/json: schema: From daeeb66aba0fcba12dc60520daee967c9e4631c9 Mon Sep 17 00:00:00 2001 From: kphurley7 Date: Thu, 17 Sep 2026 09:38:30 -0500 Subject: [PATCH 08/11] docs(care): a replay requires the session token its original required A replay under an Idempotency-Key is now authorised exactly like the request it replays. Replaying a turn sent while the chat could reach the account needs a valid session token for the chat, or it is refused with 401 CARE_SESSION_EXPIRED and returns nothing from the record. A replay of a turn that ran before the cardholder verified needs no token. A replay never returns a sessionToken, cancels a verification or sends a code. A partner who lost the token therefore cannot replay an account turn and should send the cardholder's next message as a new message, which verifies them again. The replayed and sessionToken descriptions, the message route, the session header, the 401 response, the CARE_SESSION_EXPIRED row and the overview's lost-response recovery say so. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015njn7Fen4mg9NDX7AZQRTj --- mintlify/openapi.yaml | 35 ++++++++++++------- openapi.yaml | 35 ++++++++++++------- .../schemas/care/CareChatMessageResponse.yaml | 22 +++++++----- .../components/schemas/care/CareError.yaml | 2 +- openapi/openapi.yaml | 8 +++-- .../care/care_chats_{chatId}_messages.yaml | 31 +++++++++++----- 6 files changed, 86 insertions(+), 47 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 1e7f40908..2bffe1957 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -149,9 +149,11 @@ tags: Some things are absent by construction, so no integration can come to depend on them: nothing returns a transcript or an earlier turn's reply (retrying under the original `Idempotency-Key` is how an answer that was - produced but not delivered is recovered, and such a replay never carries - a `sessionToken`; a lost token is recovered by the cardholder's next - message verifying them again), nothing sets a verification + produced but not delivered is recovered; a replay requires the same + session token its original request required and never carries a + `sessionToken`, so a lost token is recovered by sending the cardholder's + next message as a new message, which verifies them again), nothing sets + a verification state, and nothing changes the number a code is sent to. Errors from these endpoints carry `code` and `reason` only — see the @@ -11093,11 +11095,18 @@ paths: to recover an answer: nothing on this surface reads a transcript, so a reply that was produced but not delivered is reachable only by retrying under the same key, which returns the recorded answer with - `replayed: true`. A replay returns the recorded reply but never a - `sessionToken`, even if the original response carried one. If the - response carrying the token was lost, send the cardholder's next message - without a token: the verification is cancelled, a new code is sent, and - the agent asks for it. A replay is answered even after the chat has ended. + `replayed: true`. **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 nothing from + the record. 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 turn that failed does not pin its key: the same key may be sent again and will run a fresh turn. @@ -11212,7 +11221,7 @@ paths: - name: X-Ls-Care-Session in: header required: false - description: 'The `sessionToken` this route returned on the turn in which the cardholder sent their verification code. Send it on every message while the chat''s verification is current. A message without it, or with one past its expiry, is not refused: the chat''s verification is taken away before the turn runs and a new code is sent, so the cardholder verifies again in the conversation. A token that is sent is always checked for what it names: a malformed one is refused with `CARE_SESSION_EXPIRED`, one minted for another chat or another cardholder is `NOT_FOUND`, and one minted for another platform is `CARE_SESSION_PLATFORM_MISMATCH`, whether or not it has expired. An opening never reads the header at all, because a widget that reloaded has lost its token and that is the case the opening exists for.' + description: 'The `sessionToken` this route returned on the turn in which the cardholder sent their verification code. Send it on every message while the chat''s verification is current. A message without it, or with one past its expiry, is not refused: the chat''s verification is taken away before the turn runs and a new code is sent, so the cardholder verifies again in the conversation. The exception is a replay under an `Idempotency-Key` whose original request required a session: it must carry a valid token, or it is refused with `CARE_SESSION_EXPIRED` and returns nothing from the record. A token that is sent is always checked for what it names: a malformed one is refused with `CARE_SESSION_EXPIRED`, one minted for another chat or another cardholder is `NOT_FOUND`, and one minted for another platform is `CARE_SESSION_PLATFORM_MISMATCH`, whether or not it has expired. An opening never reads the header at all, because a widget that reloaded has lost its token and that is the case the opening exists for.' schema: type: string example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl @@ -11253,7 +11262,7 @@ paths: schema: $ref: '#/components/schemas/CareError' '401': - description: '`UNAUTHORIZED` when the platform''s Grid credential is missing or wrong. `CARE_SESSION_EXPIRED` when the `X-Ls-Care-Session` token presented is malformed. A missing or expired session token is never a `401`: it 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.' + description: '`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; nothing from the record 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.' content: application/json: schema: @@ -29261,7 +29270,7 @@ components: example: I can help with that. To look at your account I've texted you a code. What does it say? replayed: type: boolean - description: Whether this answer came from the record of an earlier call under the same `Idempotency-Key` rather than from a turn run now. Present only when a turn ran. There is no route that reads a transcript, so a retry under the original key is the only way to recover a reply that was produced but never delivered. A replay never carries a `sessionToken`, even if the original response did; if that response was lost, the cardholder's next message without a token verifies them again. + description: 'Whether this answer came from the record of an earlier call under the same `Idempotency-Key` rather than from a turn run now. Present only when a turn ran. There is no route that reads a transcript, so a retry under the original key is the only way to recover a reply that was produced but never delivered. A replay requires the same session token its original request required: replaying 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 replays without one. A replay never carries a `sessionToken`, even if the original response did.' example: false destinationMasked: type: string @@ -29286,7 +29295,7 @@ components: 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` returns the recorded reply but never a `sessionToken`, even if the original response carried one. If the response carrying the token was lost, send the cardholder's next message without a token: the verification is cancelled, a new code is sent, and the agent asks for it. + 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 recorded reply without one. example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl expiresAt: type: string @@ -29321,7 +29330,7 @@ components: | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `chatId` path id beginning `Customer:` that is not well formed, or a chat that cannot be verified at all. | | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused at the content boundary and no turn ran. `reason` carries the sentence to show them. | | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified — that is signalled in a `200` body. | - | `CARE_SESSION_EXPIRED` | 401 | The `X-Ls-Care-Session` token presented is malformed, or not a care session token at all. A missing or expired token is never this error: the message verifies the cardholder again inside a `200` instead. Rarely, also returned when a verification lands on the chat at the same moment a message without a live token takes it away; send the message again. | + | `CARE_SESSION_EXPIRED` | 401 | The `X-Ls-Care-Session` token presented is malformed, or not a care session token at all; or a replay under an `Idempotency-Key` lacks the valid session token its original request required, in which case nothing from the record is returned. For a new message, a missing or expired token is never this error: the message verifies the cardholder again inside a `200` instead. Rarely, also returned when a verification lands on the chat at the same moment a message without a live token takes it away; send the message again. | | `FORBIDDEN` | 403 | Embedded care is not enabled for this platform. | | `CARE_SESSION_PLATFORM_MISMATCH` | 403 | The session token was minted for a different platform. Stop; retrying will not help. | | `NOT_FOUND` | 404 | No such chat for this platform. A chat belonging to another platform, a chat that never existed, a malformed chat id, and an `X-Ls-Care-Session` token minted for a different chat or a different cardholder are one answer. | diff --git a/openapi.yaml b/openapi.yaml index 1e7f40908..2bffe1957 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -149,9 +149,11 @@ tags: Some things are absent by construction, so no integration can come to depend on them: nothing returns a transcript or an earlier turn's reply (retrying under the original `Idempotency-Key` is how an answer that was - produced but not delivered is recovered, and such a replay never carries - a `sessionToken`; a lost token is recovered by the cardholder's next - message verifying them again), nothing sets a verification + produced but not delivered is recovered; a replay requires the same + session token its original request required and never carries a + `sessionToken`, so a lost token is recovered by sending the cardholder's + next message as a new message, which verifies them again), nothing sets + a verification state, and nothing changes the number a code is sent to. Errors from these endpoints carry `code` and `reason` only — see the @@ -11093,11 +11095,18 @@ paths: to recover an answer: nothing on this surface reads a transcript, so a reply that was produced but not delivered is reachable only by retrying under the same key, which returns the recorded answer with - `replayed: true`. A replay returns the recorded reply but never a - `sessionToken`, even if the original response carried one. If the - response carrying the token was lost, send the cardholder's next message - without a token: the verification is cancelled, a new code is sent, and - the agent asks for it. A replay is answered even after the chat has ended. + `replayed: true`. **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 nothing from + the record. 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 turn that failed does not pin its key: the same key may be sent again and will run a fresh turn. @@ -11212,7 +11221,7 @@ paths: - name: X-Ls-Care-Session in: header required: false - description: 'The `sessionToken` this route returned on the turn in which the cardholder sent their verification code. Send it on every message while the chat''s verification is current. A message without it, or with one past its expiry, is not refused: the chat''s verification is taken away before the turn runs and a new code is sent, so the cardholder verifies again in the conversation. A token that is sent is always checked for what it names: a malformed one is refused with `CARE_SESSION_EXPIRED`, one minted for another chat or another cardholder is `NOT_FOUND`, and one minted for another platform is `CARE_SESSION_PLATFORM_MISMATCH`, whether or not it has expired. An opening never reads the header at all, because a widget that reloaded has lost its token and that is the case the opening exists for.' + description: 'The `sessionToken` this route returned on the turn in which the cardholder sent their verification code. Send it on every message while the chat''s verification is current. A message without it, or with one past its expiry, is not refused: the chat''s verification is taken away before the turn runs and a new code is sent, so the cardholder verifies again in the conversation. The exception is a replay under an `Idempotency-Key` whose original request required a session: it must carry a valid token, or it is refused with `CARE_SESSION_EXPIRED` and returns nothing from the record. A token that is sent is always checked for what it names: a malformed one is refused with `CARE_SESSION_EXPIRED`, one minted for another chat or another cardholder is `NOT_FOUND`, and one minted for another platform is `CARE_SESSION_PLATFORM_MISMATCH`, whether or not it has expired. An opening never reads the header at all, because a widget that reloaded has lost its token and that is the case the opening exists for.' schema: type: string example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl @@ -11253,7 +11262,7 @@ paths: schema: $ref: '#/components/schemas/CareError' '401': - description: '`UNAUTHORIZED` when the platform''s Grid credential is missing or wrong. `CARE_SESSION_EXPIRED` when the `X-Ls-Care-Session` token presented is malformed. A missing or expired session token is never a `401`: it 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.' + description: '`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; nothing from the record 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.' content: application/json: schema: @@ -29261,7 +29270,7 @@ components: example: I can help with that. To look at your account I've texted you a code. What does it say? replayed: type: boolean - description: Whether this answer came from the record of an earlier call under the same `Idempotency-Key` rather than from a turn run now. Present only when a turn ran. There is no route that reads a transcript, so a retry under the original key is the only way to recover a reply that was produced but never delivered. A replay never carries a `sessionToken`, even if the original response did; if that response was lost, the cardholder's next message without a token verifies them again. + description: 'Whether this answer came from the record of an earlier call under the same `Idempotency-Key` rather than from a turn run now. Present only when a turn ran. There is no route that reads a transcript, so a retry under the original key is the only way to recover a reply that was produced but never delivered. A replay requires the same session token its original request required: replaying 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 replays without one. A replay never carries a `sessionToken`, even if the original response did.' example: false destinationMasked: type: string @@ -29286,7 +29295,7 @@ components: 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` returns the recorded reply but never a `sessionToken`, even if the original response carried one. If the response carrying the token was lost, send the cardholder's next message without a token: the verification is cancelled, a new code is sent, and the agent asks for it. + 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 recorded reply without one. example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl expiresAt: type: string @@ -29321,7 +29330,7 @@ components: | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `chatId` path id beginning `Customer:` that is not well formed, or a chat that cannot be verified at all. | | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused at the content boundary and no turn ran. `reason` carries the sentence to show them. | | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified — that is signalled in a `200` body. | - | `CARE_SESSION_EXPIRED` | 401 | The `X-Ls-Care-Session` token presented is malformed, or not a care session token at all. A missing or expired token is never this error: the message verifies the cardholder again inside a `200` instead. Rarely, also returned when a verification lands on the chat at the same moment a message without a live token takes it away; send the message again. | + | `CARE_SESSION_EXPIRED` | 401 | The `X-Ls-Care-Session` token presented is malformed, or not a care session token at all; or a replay under an `Idempotency-Key` lacks the valid session token its original request required, in which case nothing from the record is returned. For a new message, a missing or expired token is never this error: the message verifies the cardholder again inside a `200` instead. Rarely, also returned when a verification lands on the chat at the same moment a message without a live token takes it away; send the message again. | | `FORBIDDEN` | 403 | Embedded care is not enabled for this platform. | | `CARE_SESSION_PLATFORM_MISMATCH` | 403 | The session token was minted for a different platform. Stop; retrying will not help. | | `NOT_FOUND` | 404 | No such chat for this platform. A chat belonging to another platform, a chat that never existed, a malformed chat id, and an `X-Ls-Care-Session` token minted for a different chat or a different cardholder are one answer. | diff --git a/openapi/components/schemas/care/CareChatMessageResponse.yaml b/openapi/components/schemas/care/CareChatMessageResponse.yaml index 507cb21d3..695636588 100644 --- a/openapi/components/schemas/care/CareChatMessageResponse.yaml +++ b/openapi/components/schemas/care/CareChatMessageResponse.yaml @@ -97,9 +97,12 @@ properties: same `Idempotency-Key` rather than from a turn run now. Present only when a turn ran. There is no route that reads a transcript, so a retry under the original key is the only way to recover a reply that was - produced but never delivered. A replay never carries a `sessionToken`, - even if the original response did; if that response was lost, the - cardholder's next message without a token verifies them again. + produced but never delivered. A replay requires the same session token + its original request required: replaying 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 replays without one. A replay never carries a + `sessionToken`, even if the original response did. example: false destinationMasked: type: string @@ -153,11 +156,14 @@ properties: `sessionToken`. The one you are already holding is still the right one. - A replay under the same `Idempotency-Key` returns the recorded reply - but never a `sessionToken`, even if the original response carried one. - If the response carrying the token was lost, send the cardholder's next - message without a token: the verification is cancelled, a new code is - sent, and the agent asks for it. + 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 recorded reply without one. example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl expiresAt: type: string diff --git a/openapi/components/schemas/care/CareError.yaml b/openapi/components/schemas/care/CareError.yaml index 8e8a55f99..e8aedd857 100644 --- a/openapi/components/schemas/care/CareError.yaml +++ b/openapi/components/schemas/care/CareError.yaml @@ -25,7 +25,7 @@ properties: | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `chatId` path id beginning `Customer:` that is not well formed, or a chat that cannot be verified at all. | | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused at the content boundary and no turn ran. `reason` carries the sentence to show them. | | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified — that is signalled in a `200` body. | - | `CARE_SESSION_EXPIRED` | 401 | The `X-Ls-Care-Session` token presented is malformed, or not a care session token at all. A missing or expired token is never this error: the message verifies the cardholder again inside a `200` instead. Rarely, also returned when a verification lands on the chat at the same moment a message without a live token takes it away; send the message again. | + | `CARE_SESSION_EXPIRED` | 401 | The `X-Ls-Care-Session` token presented is malformed, or not a care session token at all; or a replay under an `Idempotency-Key` lacks the valid session token its original request required, in which case nothing from the record is returned. For a new message, a missing or expired token is never this error: the message verifies the cardholder again inside a `200` instead. Rarely, also returned when a verification lands on the chat at the same moment a message without a live token takes it away; send the message again. | | `FORBIDDEN` | 403 | Embedded care is not enabled for this platform. | | `CARE_SESSION_PLATFORM_MISMATCH` | 403 | The session token was minted for a different platform. Stop; retrying will not help. | | `NOT_FOUND` | 404 | No such chat for this platform. A chat belonging to another platform, a chat that never existed, a malformed chat id, and an `X-Ls-Care-Session` token minted for a different chat or a different cardholder are one answer. | diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 2685b6755..7eaa304b2 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -185,9 +185,11 @@ tags: Some things are absent by construction, so no integration can come to depend on them: nothing returns a transcript or an earlier turn's reply (retrying under the original `Idempotency-Key` is how an answer that was - produced but not delivered is recovered, and such a replay never carries - a `sessionToken`; a lost token is recovered by the cardholder's next - message verifying them again), nothing sets a verification + produced but not delivered is recovered; a replay requires the same + session token its original request required and never carries a + `sessionToken`, so a lost token is recovered by sending the cardholder's + next message as a new message, which verifies them again), nothing sets + a verification state, and nothing changes the number a code is sent to. Errors from these endpoints carry `code` and `reason` only — see the diff --git a/openapi/paths/care/care_chats_{chatId}_messages.yaml b/openapi/paths/care/care_chats_{chatId}_messages.yaml index d04cbef63..9ac8fef14 100644 --- a/openapi/paths/care/care_chats_{chatId}_messages.yaml +++ b/openapi/paths/care/care_chats_{chatId}_messages.yaml @@ -26,11 +26,18 @@ post: to recover an answer: nothing on this surface reads a transcript, so a reply that was produced but not delivered is reachable only by retrying under the same key, which returns the recorded answer with - `replayed: true`. A replay returns the recorded reply but never a - `sessionToken`, even if the original response carried one. If the - response carrying the token was lost, send the cardholder's next message - without a token: the verification is cancelled, a new code is sent, and - the agent asks for it. A replay is answered even after the chat has ended. + `replayed: true`. **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 nothing from + the record. 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 turn that failed does not pin its key: the same key may be sent again and will run a fresh turn. @@ -159,7 +166,10 @@ post: while the chat's verification is current. A message without it, or with one past its expiry, is not refused: the chat's verification is taken away before the turn runs and a new code is sent, so the - cardholder verifies again in the conversation. A token that is sent is + cardholder verifies again in the conversation. The exception is a + replay under an `Idempotency-Key` whose original request required a + session: it must carry a valid token, or it is refused with + `CARE_SESSION_EXPIRED` and returns nothing from the record. A token that is sent is always checked for what it names: a malformed one is refused with `CARE_SESSION_EXPIRED`, one minted for another chat or another cardholder is `NOT_FOUND`, and one minted for another platform is @@ -218,9 +228,12 @@ post: description: >- `UNAUTHORIZED` when the platform's Grid credential is missing or wrong. `CARE_SESSION_EXPIRED` when the `X-Ls-Care-Session` token - presented is malformed. A missing or expired session token is never a - `401`: it verifies the cardholder again inside a `200` carrying - `verificationRequired: true`, as does an unverified or stale chat. + presented is malformed, or when a replay under an `Idempotency-Key` + lacks the valid session token its original request required; nothing + from the record 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. From 001db51efa0f342864d5afa362f6b746268a44a8 Mon Sep 17 00:00:00 2001 From: kphurley7 Date: Thu, 17 Sep 2026 10:42:57 -0500 Subject: [PATCH 09/11] docs(care): trim the message response and drop internal details The message response now documents exactly: chatId, chatEnded and verificationRequired on every body; codeLength when verification is required and the length is known; id, redactedCustomerMessage and reply when a turn ran (reply also carries the opening line); and sessionToken and expiresAt only on the verifying turn. verificationStatus, alreadySent, destinationMasked and replayed are removed, with the warning not to treat verificationStatus as permission and the now-unreferenced CareChatVerificationStatus schema and its Stainless model. displayMessage is renamed redactedCustomerMessage. verificationRequired and codeLength are documented as optional hints for a one-time-code input. A retry under the same Idempotency-Key is described as returning the original reply with the same id, never a sessionToken. Descriptions of internals are removed: billing, model calls, the content boundary, the SMS provider, and how the delete route bounds its write. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015njn7Fen4mg9NDX7AZQRTj --- .stainless/stainless.yml | 1 - mintlify/openapi.yaml | 318 ++++++++---------- openapi.yaml | 318 ++++++++---------- .../schemas/care/CareChatMessageRequest.yaml | 11 +- .../schemas/care/CareChatMessageResponse.yaml | 136 +++----- .../care/CareChatVerificationStatus.yaml | 34 -- .../components/schemas/care/CareError.yaml | 6 +- openapi/openapi.yaml | 80 ++--- openapi/paths/care/care_chats_{chatId}.yaml | 12 +- .../care/care_chats_{chatId}_messages.yaml | 161 +++++---- 10 files changed, 446 insertions(+), 631 deletions(-) delete mode 100644 openapi/components/schemas/care/CareChatVerificationStatus.yaml diff --git a/.stainless/stainless.yml b/.stainless/stainless.yml index 6c804df38..80c5086a7 100644 --- a/.stainless/stainless.yml +++ b/.stainless/stainless.yml @@ -689,7 +689,6 @@ resources: models: care_chat_message_request: "#/components/schemas/CareChatMessageRequest" care_chat_message_response: "#/components/schemas/CareChatMessageResponse" - care_chat_verification_status: "#/components/schemas/CareChatVerificationStatus" care_error: "#/components/schemas/CareError" settings: diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 2bffe1957..f07a11075 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -76,23 +76,20 @@ tags: 1. **Open the chat.** `POST /care/chats/{chatId}/messages` with an empty `message`, addressed to the cardholder as `Customer:`. Returns the chat's id and the agent's opening line in `reply`, which is what - a chat window renders before anybody has typed. Sends no SMS, runs no - turn, needs no `Idempotency-Key`, bills nothing, and is safe to call - again: a widget that reloaded gets the same chat back, this time - without a `reply`. + a chat window renders before anybody has typed. Sends no SMS, needs + no `Idempotency-Key` and consumes none, and is safe to call again: a + widget that reloaded gets the same chat back, this time without a + `reply`. 2. **Send the cardholder's first message.** The same route with their text and an `Idempotency-Key` header. One turn runs synchronously and the agent's answer comes back in `reply`. On a chat that cannot yet - reach the account this call also texts the verification code and says - so in the same body: `verificationRequired: true`, with - `destinationMasked`, `alreadySent` and `codeLength`, and the agent's - `reply` asks the cardholder for the code. - 3. **The cardholder sends the code as an ordinary message.** The agent - checks it with the SMS provider, and the response to that turn - carries `sessionToken` and `expiresAt`. Send that token as the - `X-Ls-Care-Session` header on every message from then on. A code that - does not match is answered in `reply`, and the cardholder can simply - send it again. + reach the account this call also texts the cardholder a code, and the + agent's `reply` asks for it. + 3. **The cardholder sends the code as an ordinary message.** When it is + correct, the response to that turn carries `sessionToken` and + `expiresAt`. Send that token as the `X-Ls-Care-Session` header on + every message from then on. A code that does not match is answered in + `reply`, and the cardholder can simply send it again. 4. **Close the chat** with `DELETE /care/chats/{chatId}` when the cardholder is done. Every response also carries `chatEnded`, and the agent can end the chat itself: when `chatEnded` is true, the closing @@ -101,22 +98,30 @@ tags: `409 CARE_CHAT_ENDED`; address the cardholder as `Customer:` to open a new chat. + **Render `redactedCustomerMessage` if present, then `reply` if + present.** That one rule covers an opening, a resume and a turn. + **A cardholder whose code never arrived asks the agent for another one.** The agent sends a new code to the number on file and says so in its `reply`. There is no separate route for it. - **Render `displayMessage` if present, then `reply` if present.** That one - rule covers an opening, a resume and a turn. + **The verification fields are optional hints.** `verificationRequired` + and `codeLength` let an app show 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. A partner can ignore them, since the agent asks for the + code in its reply and says where it went, and the app still sends the + code as an ordinary message. Step 1 is optional. A message addressed to a `Customer:` id opens that cardholder's chat if they have none, so a partner whose cardholder has just typed something spends one call rather than two. - A cardholder who has not verified yet is never a `401`. That state is - reported inside a `200`, because the first message from every new - cardholder is unverified and a `401` there would be indistinguishable - from a bad credential. On these endpoints a `401` means the platform's - Grid credential is wrong, or the session token presented is malformed. + A cardholder who has not verified yet is never a `401`, because the + first message from every new cardholder is unverified and a `401` there + would be indistinguishable from a bad credential. On these endpoints a + `401` means the platform's Grid credential is wrong, the session token + presented is malformed, or a replay lacks the session token its original + request required. **Verification expires, and a lost session is recoverable in the chat.** A chat reaches the cardholder's account only while its @@ -124,27 +129,27 @@ tags: default deployment, which is also how long a session token lives. Past that window a message gets a fresh code, and the agent asks the cardholder to send it back inside the conversation, which hands the - partner the next session token. A message sent with no + partner the next session token. 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 taken away before the turn runs, a new code is sent, and + verification is cancelled before the turn runs, a new code is sent, and the agent asks for it. So a client that lost its token, or held one too long, simply keeps sending messages. Only a token that is malformed, or that was minted for another platform, chat or cardholder, is refused. Two things an integration has to get right: - - **Render `displayMessage`, never your own copy of what was typed.** - Grid redacts the cardholder's text on the way in, and `displayMessage` - is that redacted echo. A UI that renders its own input box instead has - put a card number the cardholder pasted back on their screen unmasked, - and into every log the transcript reaches. + - **Render `redactedCustomerMessage`, never your own copy of what was + typed.** Grid masks card numbers and other sensitive digit strings in + it. A UI that renders its own copy of the input instead puts a card + number the cardholder pasted back on their screen unmasked, and into + your logs. - **The session token is returned once and is never re-readable.** No - endpoint hands it back, there is no revocation, and it is short-lived - by design. Hold it for the life of the cardholder's session. It - arrives on the message response to the turn in which the cardholder - sent their code, and on no other. Losing it costs the cardholder a new - code, never access to the chat. + endpoint hands it back, it cannot be revoked, and it is short-lived by + design. Hold it for the life of the cardholder's session. It arrives + on the message response to the turn in which the cardholder sent their + code, and on no other. Losing it costs the cardholder a new code, never + access to the chat. Some things are absent by construction, so no integration can come to depend on them: nothing returns a transcript or an earlier turn's reply @@ -153,12 +158,11 @@ tags: session token its original request required and never carries a `sessionToken`, so a lost token is recovered by sending the cardholder's next message as a new message, which verifies them again), nothing sets - a verification - state, and nothing changes the number a code is sent to. + a verification state, and nothing changes the number a code is sent to. - Errors from these endpoints carry `code` and `reason` only — see the - `CareError` schema, which differs from the `Error4xx` schemas used - elsewhere in this specification. + Errors from these endpoints carry `code` and `reason` only (see the + `CareError` schema), unlike the `Error4xx` schemas used elsewhere in this + specification. paths: /config: get: @@ -11084,45 +11088,52 @@ paths: post: summary: Send a care chat message description: | - 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 + 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 reads a transcript, so a + 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 recorded answer with - `replayed: true`. **A replay requires the same session token its original + 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 nothing from - the record. 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 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 nothing.** On a + 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 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`, `displayMessage` and `replayed` are absent on - both, because nothing was typed and no turn ran. Either way no code is - sent, no `Idempotency-Key` is needed or consumed, and nothing is billed, - 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:` that has ended: - it answers `409 CARE_CHAT_ENDED`, and the recovery is to address the - cardholder as `Customer:`, which opens a new chat. + 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:` that has ended: it answers + `409 CARE_CHAT_ENDED`, and the recovery is to address the cardholder as + `Customer:`, 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 @@ -11132,31 +11143,24 @@ paths: what this call did, so a replay of that turn also says true. When it is true, stop sending to that chat. - **Verification is signalled in this `200` body, never as a `401`.** Every - new cardholder's first message is unverified, so a `401` there would make - the normal case indistinguishable from a bad credential. A chat that - cannot yet reach the account answers `200` with - `verificationRequired: true`, and the first message with text is what - sends the code — `destinationMasked`, `alreadySent` and, where readable, - `codeLength` come back with it, and the agent's own sentence in `reply` - asks the cardholder for the code. Later messages on the same chat find the - code already outstanding and send nothing, reporting `alreadySent: true`. - - **Render `displayMessage` if present, then `reply` if present.** That one - rule covers every call: an opening, a resume and a turn. Grid redacts the - cardholder's text on the way in, so a card number they pasted comes back - masked, and `displayMessage` is that redacted echo. A UI that echoes its - own input box instead has put an unmasked PAN in the transcript, on the - screen and in whatever logs the transcript reaches. + **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.** The - agent checks the code with the SMS provider, and the chat comes out of - that turn able to reach the account. 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. + 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. @@ -11170,24 +11174,20 @@ paths: 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 chat no longer reaches the account, the header stops - being required, and the next message **sent without it** is treated as an - unverified one: a fresh code goes out, the body carries - `verificationRequired: true` with `destinationMasked`, `alreadySent` and - `codeLength`, and the agent's own `reply` asks the cardholder to send the - code. The turn still runs and still answers. + 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 message sent with no `X-Ls-Care-Session` header, or with a + 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 chat's verification is taken away 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 body - carries `verificationRequired: true` with `destinationMasked`, - `alreadySent` and `codeLength`, the agent asks the cardholder for the - code, 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. + 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 @@ -11197,13 +11197,11 @@ paths: widget still holding a dead token can resume with one and be told where verification stands without texting anybody. - Everything in the response describes the chat as of the moment the turn - started, `verificationStatus` included. The agent can verify a cardholder - during a turn, so a chat that this very call verified still reads - unverified here and verified on the next call. For the same reason a chat - whose verification has gone stale reports `verificationStatus: VERIFIED` - beside `verificationRequired: true`. Branch on `verificationRequired`, - never on `verificationStatus`. + 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`. operationId: sendCareChatMessage tags: - Embedded Care @@ -11213,7 +11211,7 @@ paths: - name: Idempotency-Key in: header required: false - description: 'A unique identifier for this turn, up to 255 characters. **Required whenever `message` is non-empty**; omitting it there is rejected with `INVALID_INPUT`. A retry carrying the same key returns the first call''s answer with `replayed: true`; aiming the same key at a different message, a different chat or a different cardholder is rejected with `409 CONFLICT`. An opening (empty `message`) neither needs nor consumes a key — one sent anyway is ignored, so that the partner''s first real message can still use it.' + description: 'A unique identifier for this turn, up to 255 characters. **Required whenever `message` is non-empty**; omitting it there is rejected with `INVALID_INPUT`. A retry carrying the same key returns the first call''s answer with the same `id` (subject to the session rule for replays); aiming the same key at a different message, a different chat or a different cardholder is rejected with `409 CONFLICT`. An opening (empty `message`) neither needs nor consumes a key: one sent anyway is ignored, so that the partner''s first real message can still use it.' schema: type: string maxLength: 255 @@ -11221,7 +11219,7 @@ paths: - name: X-Ls-Care-Session in: header required: false - description: 'The `sessionToken` this route returned on the turn in which the cardholder sent their verification code. Send it on every message while the chat''s verification is current. A message without it, or with one past its expiry, is not refused: the chat''s verification is taken away before the turn runs and a new code is sent, so the cardholder verifies again in the conversation. The exception is a replay under an `Idempotency-Key` whose original request required a session: it must carry a valid token, or it is refused with `CARE_SESSION_EXPIRED` and returns nothing from the record. A token that is sent is always checked for what it names: a malformed one is refused with `CARE_SESSION_EXPIRED`, one minted for another chat or another cardholder is `NOT_FOUND`, and one minted for another platform is `CARE_SESSION_PLATFORM_MISMATCH`, whether or not it has expired. An opening never reads the header at all, because a widget that reloaded has lost its token and that is the case the opening exists for.' + description: 'The `sessionToken` this route returned on the turn in which the cardholder sent their verification code. Send it on every message while the chat''s verification is current. A message without it, or with one past its expiry, is not refused: the chat''s verification is cancelled before the turn runs and a new code is sent, so the cardholder verifies again in the conversation. The exception is a replay under an `Idempotency-Key` whose original request required a session: it must carry a valid token, or it is refused with `CARE_SESSION_EXPIRED` and returns no reply. A token that is sent is always checked for what it names: a malformed one is refused with `CARE_SESSION_EXPIRED`, one minted for another chat or another cardholder is `NOT_FOUND`, and one minted for another platform is `CARE_SESSION_PLATFORM_MISMATCH`, whether or not it has expired. An opening never reads the header at all, because a widget that reloaded has lost its token and that is the case the opening exists for.' schema: type: string example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl @@ -11256,13 +11254,13 @@ paths: schema: $ref: '#/components/schemas/CareChatMessageResponse' '400': - description: 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 at the content boundary — no turn ran, and `reason` is written to be shown to them. + description: 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. content: application/json: schema: $ref: '#/components/schemas/CareError' '401': - description: '`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; nothing from the record 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.' + description: '`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.' content: application/json: schema: @@ -11292,7 +11290,7 @@ paths: schema: $ref: '#/components/schemas/CareError' '429': - description: '`CARE_BUDGET_EXCEEDED` when this chat has reached its limit. The chat is refused rather than replaced: opening a fresh one would be the limit cleared by asking again. `RATE_LIMITED` when the platform is calling this endpoint too often; back off for the interval in the `Retry-After` header.' + description: '`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.' content: application/json: schema: @@ -11338,14 +11336,12 @@ paths: with `CARE_CHAT_ENDED`, and the cardholder starts a new chat by sending another message. - No session token is required, even on a verified chat. Ending a chat is - the one thing the second factor is exempt from: it reaches nothing but the - chat's own closing timestamp, and requiring a live session would mean a - partner whose cardholder walked away could not close the chat without - texting them another code first. Ownership of the chat by the calling - platform is the whole bound. + No session token is required, even on a verified chat. Ending a chat + touches no account data, and requiring a live session would mean a partner + whose cardholder walked away could not close the chat without texting them + another code first. The chat only has to belong to the calling platform. - Closing a chat that is already closed is not an error — that is the state + Closing a chat that is already closed is not an error: that is the state the caller asked for. operationId: endCareChat tags: @@ -29183,9 +29179,9 @@ components: message: type: string description: |- - What the cardholder typed, verbatim and unredacted. Send it exactly as they entered it: Grid redacts it on the way in and returns the redacted form as `displayMessage`, which is what your UI must render. + 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 model call, no SMS, no `Idempotency-Key` consumed, nothing billed. A non-empty value runs exactly one turn synchronously and requires an `Idempotency-Key` header. + 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. example: My card was declined at the grocery store locale: type: string @@ -29193,47 +29189,11 @@ components: maxLength: 32 description: 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`. example: en-US - CareChatVerificationStatus: - type: string - enum: - - PENDING - - SENT - - VERIFIED - - TRUSTED - - FAILED - description: | - Where the cardholder's proof of presence stands on this chat, as of the - moment the call was received. - - **This status alone does not say whether the agent can reach the account.** - A chat reaches it only while it is in `VERIFIED` or `TRUSTED` *and* the - verification behind that status is newer than the session lifetime, fifteen - minutes in the default deployment. Once it ages past that window the status - still reads `VERIFIED`, the chat no longer reaches the account, the - `X-Ls-Care-Session` header stops being required, and the next message sends - a fresh code and has the agent ask for it. The field that answers "can the - agent act now" is `verificationRequired` on the message response, so branch - on that and treat this status as a record of what has happened rather than - of what is permitted. - - This field reports the chat as it was when the turn started. The agent can - verify a cardholder during a turn, so a chat that was verified by the turn - you just made still reads `SENT` in that response and `VERIFIED` on the next - call. - - | Status | Description | - |--------|-------------| - | `PENDING` | No code has been sent on this chat yet. This is where every new chat starts. | - | `SENT` | A code has been sent to the cardholder and is outstanding. | - | `VERIFIED` | The cardholder sent the code as a message and the agent confirmed it. Account-reaching tools are available for as long as that answer is newer than the session lifetime, and the session token was issued on the message response to that turn. | - | `TRUSTED` | The cardholder is treated as present without an SMS code on this chat. Reserved for channels Grid can already attribute; a partner chat does not start here. Bounded by the same freshness window as `VERIFIED`. | - | `FAILED` | The last code was answered incorrectly or expired. The cardholder can send the code again, or ask the agent for a new one. | CareChatMessageResponse: type: object - description: 'The result of one call to the message route. Which fields are present depends on what the call did: a turn adds `id`, `displayMessage`, `reply` and `replayed`; the call that opened the chat adds `reply` alone, carrying the agent''s opening line; an outstanding verification adds `destinationMasked`, `alreadySent` and, where the service could be read, `codeLength`; and the turn in which the cardholder sent their verification code adds `sessionToken` and `expiresAt`. Render `displayMessage` if present, then `reply` if present.' + description: '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.' required: - chatId - - verificationStatus - verificationRequired - chatEnded properties: @@ -29241,8 +29201,6 @@ components: type: string description: 'The chat this call ran on. When the request addressed a cardholder (`Customer:` 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.' example: CareChat:019542f5-b3e7-1d02-0000-000000000030 - verificationStatus: - $ref: '#/components/schemas/CareChatVerificationStatus' chatEnded: type: boolean description: '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:`, which opens a new chat.' @@ -29250,52 +29208,38 @@ components: verificationRequired: type: boolean description: |- - Whether the cardholder still has to prove they are present before the agent can reach their account. Present on every response, true or false, so a client reads a field rather than an absence. When true, `destinationMasked` and `alreadySent` are present, and the cardholder completes verification by sending the code as an ordinary message. A verification that is outstanding, or one taken away because a message arrived without a live session token, is never reported as a `401`: on this surface a `401` means the platform credential is wrong, or the session token presented is malformed. - - Read this field rather than `verificationStatus`. A chat whose verification has aged past the session lifetime reports `verificationStatus: VERIFIED` and `verificationRequired: true` at the same time, and that combination is the truthful one: the column records that a code was once answered, and this field records whether the agent can act on the account now. + 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. example: true id: type: string - description: Id of the cardholder's recorded message. Present only when a turn ran, so absent on an opening or a resume. + description: 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. example: CareMessage:019542f5-b3e7-1d02-0000-000000000032 - displayMessage: + redactedCustomerMessage: type: string - description: The redacted echo of what the cardholder typed, and **the string your app must render in the transcript**. Grid masks card numbers and other sensitive digit strings here, so rendering your own copy of the input instead puts a PAN the cardholder pasted back on their screen and into your logs. Present only when a turn ran. + description: '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.' example: My card [redacted card number ending 1111] was declined reply: type: string - description: '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`, consumes none and bills nothing, even though it carries a `reply`. 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.' + description: '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.' example: I can help with that. To look at your account I've texted you a code. What does it say? - replayed: - type: boolean - description: 'Whether this answer came from the record of an earlier call under the same `Idempotency-Key` rather than from a turn run now. Present only when a turn ran. There is no route that reads a transcript, so a retry under the original key is the only way to recover a reply that was produced but never delivered. A replay requires the same session token its original request required: replaying 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 replays without one. A replay never carries a `sessionToken`, even if the original response did.' - example: false - destinationMasked: - type: string - description: The masked phone number the code was sent to, for a UI to show as "we texted ...". Present whenever `verificationRequired` is true. The destination is the number already on file for the cardholder and cannot be nominated by the caller. - example: '***0001' - alreadySent: - type: boolean - description: 'Whether a code was already outstanding, so this call sent no new SMS. Present whenever `verificationRequired` is true. It reads against the call you made: on an opening, true means some earlier call left a code outstanding; on a message, true means an earlier message already sent one. Say "we already texted you" rather than implying a fresh send.' - example: false codeLength: type: integer - description: Number of digits in the code, read from the verification service's own configuration. Absent when it could not be read — leave the input unbounded in that case rather than guessing, since a guess that disagrees with the service makes every correct code look like a typo. + description: '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.' example: 6 sessionToken: type: string description: |- 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. The agent checks it with the SMS provider, and the chat comes out of that turn able to reach the account. This field is how the partner is handed the credential that turn earned, and it is the only place a session token is ever issued. A later message sent without it takes the verification away again and asks the cardholder for a new code. + 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, there is no revocation, 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`. + 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 recorded reply without 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. example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl expiresAt: type: string @@ -29328,7 +29272,7 @@ components: | Error Code | HTTP | Description | |------------|------|-------------| | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `chatId` path id beginning `Customer:` that is not well formed, or a chat that cannot be verified at all. | - | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused at the content boundary and no turn ran. `reason` carries the sentence to show them. | + | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused, for example because it is too long, and no turn ran. `reason` carries the sentence to show them. | | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified — that is signalled in a `200` body. | | `CARE_SESSION_EXPIRED` | 401 | The `X-Ls-Care-Session` token presented is malformed, or not a care session token at all; or a replay under an `Idempotency-Key` lacks the valid session token its original request required, in which case nothing from the record is returned. For a new message, a missing or expired token is never this error: the message verifies the cardholder again inside a `200` instead. Rarely, also returned when a verification lands on the chat at the same moment a message without a live token takes it away; send the message again. | | `FORBIDDEN` | 403 | Embedded care is not enabled for this platform. | @@ -29338,11 +29282,11 @@ components: | `CONFLICT` | 409 | The `Idempotency-Key` was already used for a different message or a different chat — use a new key — or the chat has no cardholder bound to it. | | `CARE_CHAT_ENDED` | 409 | The chat has ended, whether the agent ended it or it was closed, and it accepts no further messages or openings; a replay of a turn that already completed is still answered. Open a new one by sending an empty message to the cardholder as `Customer:`. | | `CARE_NOT_CONFIGURED_FOR_PLATFORM` | 422 | The platform is entitled to embedded care but has no care programme configured. | - | `CARE_BUDGET_EXCEEDED` | 429 | This chat has reached its limit. It is not replaced by a new one: a replacement would be the limit cleared by asking again. | + | `CARE_BUDGET_EXCEEDED` | 429 | This chat has reached its limit. Opening a new chat for the cardholder does not clear it. | | `RATE_LIMITED` | 429 | The platform is calling this endpoint too often. Back off for the interval in the `Retry-After` response header. | | `INTERNAL_ERROR` | 500 | Unexpected server error. | | `CARE_TURN_FAILED` | 502 | The agent could not answer. Transient; the same `Idempotency-Key` may be sent again. | - | `CARE_VERIFICATION_UNAVAILABLE` | 502 | The SMS provider could not be reached. Distinct from a wrong code: the factor is down, so retry rather than asking the cardholder again. | + | `CARE_VERIFICATION_UNAVAILABLE` | 502 | The verification code could not be sent or checked right now. Distinct from a wrong code: the problem is temporary and not the cardholder's, so retry rather than asking them again. | | `SERVICE_UNAVAILABLE` | 503 | Embedded care is not available in this deployment, or the support agent is temporarily turned off. | | `CARE_TURN_TIMEOUT` | 504 | The turn is still running. Retry with the same `Idempotency-Key` to collect the answer. | enum: diff --git a/openapi.yaml b/openapi.yaml index 2bffe1957..f07a11075 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -76,23 +76,20 @@ tags: 1. **Open the chat.** `POST /care/chats/{chatId}/messages` with an empty `message`, addressed to the cardholder as `Customer:`. Returns the chat's id and the agent's opening line in `reply`, which is what - a chat window renders before anybody has typed. Sends no SMS, runs no - turn, needs no `Idempotency-Key`, bills nothing, and is safe to call - again: a widget that reloaded gets the same chat back, this time - without a `reply`. + a chat window renders before anybody has typed. Sends no SMS, needs + no `Idempotency-Key` and consumes none, and is safe to call again: a + widget that reloaded gets the same chat back, this time without a + `reply`. 2. **Send the cardholder's first message.** The same route with their text and an `Idempotency-Key` header. One turn runs synchronously and the agent's answer comes back in `reply`. On a chat that cannot yet - reach the account this call also texts the verification code and says - so in the same body: `verificationRequired: true`, with - `destinationMasked`, `alreadySent` and `codeLength`, and the agent's - `reply` asks the cardholder for the code. - 3. **The cardholder sends the code as an ordinary message.** The agent - checks it with the SMS provider, and the response to that turn - carries `sessionToken` and `expiresAt`. Send that token as the - `X-Ls-Care-Session` header on every message from then on. A code that - does not match is answered in `reply`, and the cardholder can simply - send it again. + reach the account this call also texts the cardholder a code, and the + agent's `reply` asks for it. + 3. **The cardholder sends the code as an ordinary message.** When it is + correct, the response to that turn carries `sessionToken` and + `expiresAt`. Send that token as the `X-Ls-Care-Session` header on + every message from then on. A code that does not match is answered in + `reply`, and the cardholder can simply send it again. 4. **Close the chat** with `DELETE /care/chats/{chatId}` when the cardholder is done. Every response also carries `chatEnded`, and the agent can end the chat itself: when `chatEnded` is true, the closing @@ -101,22 +98,30 @@ tags: `409 CARE_CHAT_ENDED`; address the cardholder as `Customer:` to open a new chat. + **Render `redactedCustomerMessage` if present, then `reply` if + present.** That one rule covers an opening, a resume and a turn. + **A cardholder whose code never arrived asks the agent for another one.** The agent sends a new code to the number on file and says so in its `reply`. There is no separate route for it. - **Render `displayMessage` if present, then `reply` if present.** That one - rule covers an opening, a resume and a turn. + **The verification fields are optional hints.** `verificationRequired` + and `codeLength` let an app show 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. A partner can ignore them, since the agent asks for the + code in its reply and says where it went, and the app still sends the + code as an ordinary message. Step 1 is optional. A message addressed to a `Customer:` id opens that cardholder's chat if they have none, so a partner whose cardholder has just typed something spends one call rather than two. - A cardholder who has not verified yet is never a `401`. That state is - reported inside a `200`, because the first message from every new - cardholder is unverified and a `401` there would be indistinguishable - from a bad credential. On these endpoints a `401` means the platform's - Grid credential is wrong, or the session token presented is malformed. + A cardholder who has not verified yet is never a `401`, because the + first message from every new cardholder is unverified and a `401` there + would be indistinguishable from a bad credential. On these endpoints a + `401` means the platform's Grid credential is wrong, the session token + presented is malformed, or a replay lacks the session token its original + request required. **Verification expires, and a lost session is recoverable in the chat.** A chat reaches the cardholder's account only while its @@ -124,27 +129,27 @@ tags: default deployment, which is also how long a session token lives. Past that window a message gets a fresh code, and the agent asks the cardholder to send it back inside the conversation, which hands the - partner the next session token. A message sent with no + partner the next session token. 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 taken away before the turn runs, a new code is sent, and + verification is cancelled before the turn runs, a new code is sent, and the agent asks for it. So a client that lost its token, or held one too long, simply keeps sending messages. Only a token that is malformed, or that was minted for another platform, chat or cardholder, is refused. Two things an integration has to get right: - - **Render `displayMessage`, never your own copy of what was typed.** - Grid redacts the cardholder's text on the way in, and `displayMessage` - is that redacted echo. A UI that renders its own input box instead has - put a card number the cardholder pasted back on their screen unmasked, - and into every log the transcript reaches. + - **Render `redactedCustomerMessage`, never your own copy of what was + typed.** Grid masks card numbers and other sensitive digit strings in + it. A UI that renders its own copy of the input instead puts a card + number the cardholder pasted back on their screen unmasked, and into + your logs. - **The session token is returned once and is never re-readable.** No - endpoint hands it back, there is no revocation, and it is short-lived - by design. Hold it for the life of the cardholder's session. It - arrives on the message response to the turn in which the cardholder - sent their code, and on no other. Losing it costs the cardholder a new - code, never access to the chat. + endpoint hands it back, it cannot be revoked, and it is short-lived by + design. Hold it for the life of the cardholder's session. It arrives + on the message response to the turn in which the cardholder sent their + code, and on no other. Losing it costs the cardholder a new code, never + access to the chat. Some things are absent by construction, so no integration can come to depend on them: nothing returns a transcript or an earlier turn's reply @@ -153,12 +158,11 @@ tags: session token its original request required and never carries a `sessionToken`, so a lost token is recovered by sending the cardholder's next message as a new message, which verifies them again), nothing sets - a verification - state, and nothing changes the number a code is sent to. + a verification state, and nothing changes the number a code is sent to. - Errors from these endpoints carry `code` and `reason` only — see the - `CareError` schema, which differs from the `Error4xx` schemas used - elsewhere in this specification. + Errors from these endpoints carry `code` and `reason` only (see the + `CareError` schema), unlike the `Error4xx` schemas used elsewhere in this + specification. paths: /config: get: @@ -11084,45 +11088,52 @@ paths: post: summary: Send a care chat message description: | - 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 + 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 reads a transcript, so a + 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 recorded answer with - `replayed: true`. **A replay requires the same session token its original + 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 nothing from - the record. 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 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 nothing.** On a + 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 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`, `displayMessage` and `replayed` are absent on - both, because nothing was typed and no turn ran. Either way no code is - sent, no `Idempotency-Key` is needed or consumed, and nothing is billed, - 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:` that has ended: - it answers `409 CARE_CHAT_ENDED`, and the recovery is to address the - cardholder as `Customer:`, which opens a new chat. + 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:` that has ended: it answers + `409 CARE_CHAT_ENDED`, and the recovery is to address the cardholder as + `Customer:`, 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 @@ -11132,31 +11143,24 @@ paths: what this call did, so a replay of that turn also says true. When it is true, stop sending to that chat. - **Verification is signalled in this `200` body, never as a `401`.** Every - new cardholder's first message is unverified, so a `401` there would make - the normal case indistinguishable from a bad credential. A chat that - cannot yet reach the account answers `200` with - `verificationRequired: true`, and the first message with text is what - sends the code — `destinationMasked`, `alreadySent` and, where readable, - `codeLength` come back with it, and the agent's own sentence in `reply` - asks the cardholder for the code. Later messages on the same chat find the - code already outstanding and send nothing, reporting `alreadySent: true`. - - **Render `displayMessage` if present, then `reply` if present.** That one - rule covers every call: an opening, a resume and a turn. Grid redacts the - cardholder's text on the way in, so a card number they pasted comes back - masked, and `displayMessage` is that redacted echo. A UI that echoes its - own input box instead has put an unmasked PAN in the transcript, on the - screen and in whatever logs the transcript reaches. + **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.** The - agent checks the code with the SMS provider, and the chat comes out of - that turn able to reach the account. 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. + 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. @@ -11170,24 +11174,20 @@ paths: 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 chat no longer reaches the account, the header stops - being required, and the next message **sent without it** is treated as an - unverified one: a fresh code goes out, the body carries - `verificationRequired: true` with `destinationMasked`, `alreadySent` and - `codeLength`, and the agent's own `reply` asks the cardholder to send the - code. The turn still runs and still answers. + 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 message sent with no `X-Ls-Care-Session` header, or with a + 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 chat's verification is taken away 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 body - carries `verificationRequired: true` with `destinationMasked`, - `alreadySent` and `codeLength`, the agent asks the cardholder for the - code, 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. + 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 @@ -11197,13 +11197,11 @@ paths: widget still holding a dead token can resume with one and be told where verification stands without texting anybody. - Everything in the response describes the chat as of the moment the turn - started, `verificationStatus` included. The agent can verify a cardholder - during a turn, so a chat that this very call verified still reads - unverified here and verified on the next call. For the same reason a chat - whose verification has gone stale reports `verificationStatus: VERIFIED` - beside `verificationRequired: true`. Branch on `verificationRequired`, - never on `verificationStatus`. + 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`. operationId: sendCareChatMessage tags: - Embedded Care @@ -11213,7 +11211,7 @@ paths: - name: Idempotency-Key in: header required: false - description: 'A unique identifier for this turn, up to 255 characters. **Required whenever `message` is non-empty**; omitting it there is rejected with `INVALID_INPUT`. A retry carrying the same key returns the first call''s answer with `replayed: true`; aiming the same key at a different message, a different chat or a different cardholder is rejected with `409 CONFLICT`. An opening (empty `message`) neither needs nor consumes a key — one sent anyway is ignored, so that the partner''s first real message can still use it.' + description: 'A unique identifier for this turn, up to 255 characters. **Required whenever `message` is non-empty**; omitting it there is rejected with `INVALID_INPUT`. A retry carrying the same key returns the first call''s answer with the same `id` (subject to the session rule for replays); aiming the same key at a different message, a different chat or a different cardholder is rejected with `409 CONFLICT`. An opening (empty `message`) neither needs nor consumes a key: one sent anyway is ignored, so that the partner''s first real message can still use it.' schema: type: string maxLength: 255 @@ -11221,7 +11219,7 @@ paths: - name: X-Ls-Care-Session in: header required: false - description: 'The `sessionToken` this route returned on the turn in which the cardholder sent their verification code. Send it on every message while the chat''s verification is current. A message without it, or with one past its expiry, is not refused: the chat''s verification is taken away before the turn runs and a new code is sent, so the cardholder verifies again in the conversation. The exception is a replay under an `Idempotency-Key` whose original request required a session: it must carry a valid token, or it is refused with `CARE_SESSION_EXPIRED` and returns nothing from the record. A token that is sent is always checked for what it names: a malformed one is refused with `CARE_SESSION_EXPIRED`, one minted for another chat or another cardholder is `NOT_FOUND`, and one minted for another platform is `CARE_SESSION_PLATFORM_MISMATCH`, whether or not it has expired. An opening never reads the header at all, because a widget that reloaded has lost its token and that is the case the opening exists for.' + description: 'The `sessionToken` this route returned on the turn in which the cardholder sent their verification code. Send it on every message while the chat''s verification is current. A message without it, or with one past its expiry, is not refused: the chat''s verification is cancelled before the turn runs and a new code is sent, so the cardholder verifies again in the conversation. The exception is a replay under an `Idempotency-Key` whose original request required a session: it must carry a valid token, or it is refused with `CARE_SESSION_EXPIRED` and returns no reply. A token that is sent is always checked for what it names: a malformed one is refused with `CARE_SESSION_EXPIRED`, one minted for another chat or another cardholder is `NOT_FOUND`, and one minted for another platform is `CARE_SESSION_PLATFORM_MISMATCH`, whether or not it has expired. An opening never reads the header at all, because a widget that reloaded has lost its token and that is the case the opening exists for.' schema: type: string example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl @@ -11256,13 +11254,13 @@ paths: schema: $ref: '#/components/schemas/CareChatMessageResponse' '400': - description: 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 at the content boundary — no turn ran, and `reason` is written to be shown to them. + description: 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. content: application/json: schema: $ref: '#/components/schemas/CareError' '401': - description: '`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; nothing from the record 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.' + description: '`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.' content: application/json: schema: @@ -11292,7 +11290,7 @@ paths: schema: $ref: '#/components/schemas/CareError' '429': - description: '`CARE_BUDGET_EXCEEDED` when this chat has reached its limit. The chat is refused rather than replaced: opening a fresh one would be the limit cleared by asking again. `RATE_LIMITED` when the platform is calling this endpoint too often; back off for the interval in the `Retry-After` header.' + description: '`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.' content: application/json: schema: @@ -11338,14 +11336,12 @@ paths: with `CARE_CHAT_ENDED`, and the cardholder starts a new chat by sending another message. - No session token is required, even on a verified chat. Ending a chat is - the one thing the second factor is exempt from: it reaches nothing but the - chat's own closing timestamp, and requiring a live session would mean a - partner whose cardholder walked away could not close the chat without - texting them another code first. Ownership of the chat by the calling - platform is the whole bound. + No session token is required, even on a verified chat. Ending a chat + touches no account data, and requiring a live session would mean a partner + whose cardholder walked away could not close the chat without texting them + another code first. The chat only has to belong to the calling platform. - Closing a chat that is already closed is not an error — that is the state + Closing a chat that is already closed is not an error: that is the state the caller asked for. operationId: endCareChat tags: @@ -29183,9 +29179,9 @@ components: message: type: string description: |- - What the cardholder typed, verbatim and unredacted. Send it exactly as they entered it: Grid redacts it on the way in and returns the redacted form as `displayMessage`, which is what your UI must render. + 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 model call, no SMS, no `Idempotency-Key` consumed, nothing billed. A non-empty value runs exactly one turn synchronously and requires an `Idempotency-Key` header. + 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. example: My card was declined at the grocery store locale: type: string @@ -29193,47 +29189,11 @@ components: maxLength: 32 description: 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`. example: en-US - CareChatVerificationStatus: - type: string - enum: - - PENDING - - SENT - - VERIFIED - - TRUSTED - - FAILED - description: | - Where the cardholder's proof of presence stands on this chat, as of the - moment the call was received. - - **This status alone does not say whether the agent can reach the account.** - A chat reaches it only while it is in `VERIFIED` or `TRUSTED` *and* the - verification behind that status is newer than the session lifetime, fifteen - minutes in the default deployment. Once it ages past that window the status - still reads `VERIFIED`, the chat no longer reaches the account, the - `X-Ls-Care-Session` header stops being required, and the next message sends - a fresh code and has the agent ask for it. The field that answers "can the - agent act now" is `verificationRequired` on the message response, so branch - on that and treat this status as a record of what has happened rather than - of what is permitted. - - This field reports the chat as it was when the turn started. The agent can - verify a cardholder during a turn, so a chat that was verified by the turn - you just made still reads `SENT` in that response and `VERIFIED` on the next - call. - - | Status | Description | - |--------|-------------| - | `PENDING` | No code has been sent on this chat yet. This is where every new chat starts. | - | `SENT` | A code has been sent to the cardholder and is outstanding. | - | `VERIFIED` | The cardholder sent the code as a message and the agent confirmed it. Account-reaching tools are available for as long as that answer is newer than the session lifetime, and the session token was issued on the message response to that turn. | - | `TRUSTED` | The cardholder is treated as present without an SMS code on this chat. Reserved for channels Grid can already attribute; a partner chat does not start here. Bounded by the same freshness window as `VERIFIED`. | - | `FAILED` | The last code was answered incorrectly or expired. The cardholder can send the code again, or ask the agent for a new one. | CareChatMessageResponse: type: object - description: 'The result of one call to the message route. Which fields are present depends on what the call did: a turn adds `id`, `displayMessage`, `reply` and `replayed`; the call that opened the chat adds `reply` alone, carrying the agent''s opening line; an outstanding verification adds `destinationMasked`, `alreadySent` and, where the service could be read, `codeLength`; and the turn in which the cardholder sent their verification code adds `sessionToken` and `expiresAt`. Render `displayMessage` if present, then `reply` if present.' + description: '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.' required: - chatId - - verificationStatus - verificationRequired - chatEnded properties: @@ -29241,8 +29201,6 @@ components: type: string description: 'The chat this call ran on. When the request addressed a cardholder (`Customer:` 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.' example: CareChat:019542f5-b3e7-1d02-0000-000000000030 - verificationStatus: - $ref: '#/components/schemas/CareChatVerificationStatus' chatEnded: type: boolean description: '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:`, which opens a new chat.' @@ -29250,52 +29208,38 @@ components: verificationRequired: type: boolean description: |- - Whether the cardholder still has to prove they are present before the agent can reach their account. Present on every response, true or false, so a client reads a field rather than an absence. When true, `destinationMasked` and `alreadySent` are present, and the cardholder completes verification by sending the code as an ordinary message. A verification that is outstanding, or one taken away because a message arrived without a live session token, is never reported as a `401`: on this surface a `401` means the platform credential is wrong, or the session token presented is malformed. - - Read this field rather than `verificationStatus`. A chat whose verification has aged past the session lifetime reports `verificationStatus: VERIFIED` and `verificationRequired: true` at the same time, and that combination is the truthful one: the column records that a code was once answered, and this field records whether the agent can act on the account now. + 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. example: true id: type: string - description: Id of the cardholder's recorded message. Present only when a turn ran, so absent on an opening or a resume. + description: 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. example: CareMessage:019542f5-b3e7-1d02-0000-000000000032 - displayMessage: + redactedCustomerMessage: type: string - description: The redacted echo of what the cardholder typed, and **the string your app must render in the transcript**. Grid masks card numbers and other sensitive digit strings here, so rendering your own copy of the input instead puts a PAN the cardholder pasted back on their screen and into your logs. Present only when a turn ran. + description: '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.' example: My card [redacted card number ending 1111] was declined reply: type: string - description: '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`, consumes none and bills nothing, even though it carries a `reply`. 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.' + description: '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.' example: I can help with that. To look at your account I've texted you a code. What does it say? - replayed: - type: boolean - description: 'Whether this answer came from the record of an earlier call under the same `Idempotency-Key` rather than from a turn run now. Present only when a turn ran. There is no route that reads a transcript, so a retry under the original key is the only way to recover a reply that was produced but never delivered. A replay requires the same session token its original request required: replaying 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 replays without one. A replay never carries a `sessionToken`, even if the original response did.' - example: false - destinationMasked: - type: string - description: The masked phone number the code was sent to, for a UI to show as "we texted ...". Present whenever `verificationRequired` is true. The destination is the number already on file for the cardholder and cannot be nominated by the caller. - example: '***0001' - alreadySent: - type: boolean - description: 'Whether a code was already outstanding, so this call sent no new SMS. Present whenever `verificationRequired` is true. It reads against the call you made: on an opening, true means some earlier call left a code outstanding; on a message, true means an earlier message already sent one. Say "we already texted you" rather than implying a fresh send.' - example: false codeLength: type: integer - description: Number of digits in the code, read from the verification service's own configuration. Absent when it could not be read — leave the input unbounded in that case rather than guessing, since a guess that disagrees with the service makes every correct code look like a typo. + description: '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.' example: 6 sessionToken: type: string description: |- 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. The agent checks it with the SMS provider, and the chat comes out of that turn able to reach the account. This field is how the partner is handed the credential that turn earned, and it is the only place a session token is ever issued. A later message sent without it takes the verification away again and asks the cardholder for a new code. + 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, there is no revocation, 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`. + 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 recorded reply without 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. example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl expiresAt: type: string @@ -29328,7 +29272,7 @@ components: | Error Code | HTTP | Description | |------------|------|-------------| | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `chatId` path id beginning `Customer:` that is not well formed, or a chat that cannot be verified at all. | - | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused at the content boundary and no turn ran. `reason` carries the sentence to show them. | + | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused, for example because it is too long, and no turn ran. `reason` carries the sentence to show them. | | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified — that is signalled in a `200` body. | | `CARE_SESSION_EXPIRED` | 401 | The `X-Ls-Care-Session` token presented is malformed, or not a care session token at all; or a replay under an `Idempotency-Key` lacks the valid session token its original request required, in which case nothing from the record is returned. For a new message, a missing or expired token is never this error: the message verifies the cardholder again inside a `200` instead. Rarely, also returned when a verification lands on the chat at the same moment a message without a live token takes it away; send the message again. | | `FORBIDDEN` | 403 | Embedded care is not enabled for this platform. | @@ -29338,11 +29282,11 @@ components: | `CONFLICT` | 409 | The `Idempotency-Key` was already used for a different message or a different chat — use a new key — or the chat has no cardholder bound to it. | | `CARE_CHAT_ENDED` | 409 | The chat has ended, whether the agent ended it or it was closed, and it accepts no further messages or openings; a replay of a turn that already completed is still answered. Open a new one by sending an empty message to the cardholder as `Customer:`. | | `CARE_NOT_CONFIGURED_FOR_PLATFORM` | 422 | The platform is entitled to embedded care but has no care programme configured. | - | `CARE_BUDGET_EXCEEDED` | 429 | This chat has reached its limit. It is not replaced by a new one: a replacement would be the limit cleared by asking again. | + | `CARE_BUDGET_EXCEEDED` | 429 | This chat has reached its limit. Opening a new chat for the cardholder does not clear it. | | `RATE_LIMITED` | 429 | The platform is calling this endpoint too often. Back off for the interval in the `Retry-After` response header. | | `INTERNAL_ERROR` | 500 | Unexpected server error. | | `CARE_TURN_FAILED` | 502 | The agent could not answer. Transient; the same `Idempotency-Key` may be sent again. | - | `CARE_VERIFICATION_UNAVAILABLE` | 502 | The SMS provider could not be reached. Distinct from a wrong code: the factor is down, so retry rather than asking the cardholder again. | + | `CARE_VERIFICATION_UNAVAILABLE` | 502 | The verification code could not be sent or checked right now. Distinct from a wrong code: the problem is temporary and not the cardholder's, so retry rather than asking them again. | | `SERVICE_UNAVAILABLE` | 503 | Embedded care is not available in this deployment, or the support agent is temporarily turned off. | | `CARE_TURN_TIMEOUT` | 504 | The turn is still running. Retry with the same `Idempotency-Key` to collect the answer. | enum: diff --git a/openapi/components/schemas/care/CareChatMessageRequest.yaml b/openapi/components/schemas/care/CareChatMessageRequest.yaml index a67b93d54..a33b88462 100644 --- a/openapi/components/schemas/care/CareChatMessageRequest.yaml +++ b/openapi/components/schemas/care/CareChatMessageRequest.yaml @@ -9,14 +9,15 @@ properties: type: string description: >- What the cardholder typed, verbatim and unredacted. Send it exactly as - they entered it: Grid redacts it on the way in and returns the redacted - form as `displayMessage`, which is what your UI must render. + 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 model call, no SMS, no `Idempotency-Key` - consumed, nothing billed. A non-empty value runs exactly one turn - synchronously and requires an `Idempotency-Key` header. + 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. example: My card was declined at the grocery store locale: type: string diff --git a/openapi/components/schemas/care/CareChatMessageResponse.yaml b/openapi/components/schemas/care/CareChatMessageResponse.yaml index 695636588..15c104950 100644 --- a/openapi/components/schemas/care/CareChatMessageResponse.yaml +++ b/openapi/components/schemas/care/CareChatMessageResponse.yaml @@ -1,16 +1,15 @@ type: object description: >- The result of one call to the message route. Which fields are present - depends on what the call did: a turn adds `id`, `displayMessage`, `reply` - and `replayed`; the call that opened the chat adds `reply` alone, carrying - the agent's opening line; an outstanding verification adds - `destinationMasked`, `alreadySent` and, where the service could be read, - `codeLength`; and the turn in which the - cardholder sent their verification code adds `sessionToken` and - `expiresAt`. Render `displayMessage` if present, then `reply` if 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. required: - chatId - - verificationStatus - verificationRequired - chatEnded properties: @@ -22,8 +21,6 @@ properties: opened for them. Use it for every later call on this conversation: later messages and closing the chat. example: CareChat:019542f5-b3e7-1d02-0000-000000000030 - verificationStatus: - $ref: ./CareChatVerificationStatus.yaml chatEnded: type: boolean description: >- @@ -39,23 +36,12 @@ properties: verificationRequired: type: boolean description: >- - Whether the cardholder still has to prove they are present before the - agent can reach their account. Present on every response, true or - false, so a client reads a field rather than an absence. When true, - `destinationMasked` and `alreadySent` are present, and the cardholder - completes verification by sending the code as an ordinary message. A - verification that is outstanding, or one taken away because a message - arrived without a live session token, is never reported as a `401`: on - this surface a `401` means the platform credential is wrong, or the - session token presented is malformed. - - - Read this field rather than `verificationStatus`. A chat whose - verification has aged past the session lifetime reports - `verificationStatus: VERIFIED` and `verificationRequired: true` at the - same time, and that combination is the truthful one: the column records - that a code was once answered, and this field records whether the agent - can act on the account now. + 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 @@ -67,17 +53,19 @@ properties: id: type: string description: >- - Id of the cardholder's recorded message. Present only when a turn ran, - so absent on an opening or a resume. + 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. example: CareMessage:019542f5-b3e7-1d02-0000-000000000032 - displayMessage: + redactedCustomerMessage: type: string description: >- - The redacted echo of what the cardholder typed, and **the string your - app must render in the transcript**. Grid masks card numbers and other - sensitive digit strings here, so rendering your own copy of the input - instead puts a PAN the cardholder pasted back on their screen and into - your logs. Present only when a turn ran. + 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. example: My card [redacted card number ending 1111] was declined reply: type: string @@ -85,49 +73,27 @@ properties: 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`, consumes none and bills - nothing, even though it carries a `reply`. 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. - example: I can help with that. To look at your account I've texted you a code. What does it say? - replayed: - type: boolean - description: >- - Whether this answer came from the record of an earlier call under the - same `Idempotency-Key` rather than from a turn run now. Present only - when a turn ran. There is no route that reads a transcript, so a retry - under the original key is the only way to recover a reply that was - produced but never delivered. A replay requires the same session token - its original request required: replaying a turn sent while the chat + 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 replays without one. A replay never carries a - `sessionToken`, even if the original response did. - example: false - destinationMasked: - type: string - description: >- - The masked phone number the code was sent to, for a UI to show as "we - texted ...". Present whenever `verificationRequired` is true. The - destination is the number already on file for the cardholder and cannot - be nominated by the caller. - example: '***0001' - alreadySent: - type: boolean - description: >- - Whether a code was already outstanding, so this call sent no new SMS. - Present whenever `verificationRequired` is true. It reads against the - call you made: on an opening, true means some earlier call left a code - outstanding; on a message, true means an earlier message already sent - one. Say "we already texted you" rather than implying a fresh send. - example: false + 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. + example: I can help with that. To look at your account I've texted you a code. What does it say? codeLength: type: integer description: >- - Number of digits in the code, read from the verification service's own - configuration. Absent when it could not be read — leave the input - unbounded in that case rather than guessing, since a guess that - disagrees with the service makes every correct code look like a typo. + 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. example: 6 sessionToken: type: string @@ -137,18 +103,17 @@ properties: The cardholder completes verification by sending the SMS code as an - ordinary message. The agent checks it with the SMS provider, and the - chat comes out of that turn able to reach the account. This field is - how the partner is handed the credential that turn earned, and it is the - only place a session token is ever issued. A later message sent without - it takes the verification away again and asks the cardholder for a new - code. + 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, there is no revocation, 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`. + 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 @@ -163,7 +128,7 @@ properties: 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 recorded reply without one. + their code needs no token and returns the original reply without one. example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl expiresAt: type: string @@ -171,6 +136,7 @@ properties: description: >- 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. + 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. example: '2026-09-12T14:25:00Z' diff --git a/openapi/components/schemas/care/CareChatVerificationStatus.yaml b/openapi/components/schemas/care/CareChatVerificationStatus.yaml deleted file mode 100644 index eb52bcdcb..000000000 --- a/openapi/components/schemas/care/CareChatVerificationStatus.yaml +++ /dev/null @@ -1,34 +0,0 @@ -type: string -enum: - - PENDING - - SENT - - VERIFIED - - TRUSTED - - FAILED -description: | - Where the cardholder's proof of presence stands on this chat, as of the - moment the call was received. - - **This status alone does not say whether the agent can reach the account.** - A chat reaches it only while it is in `VERIFIED` or `TRUSTED` *and* the - verification behind that status is newer than the session lifetime, fifteen - minutes in the default deployment. Once it ages past that window the status - still reads `VERIFIED`, the chat no longer reaches the account, the - `X-Ls-Care-Session` header stops being required, and the next message sends - a fresh code and has the agent ask for it. The field that answers "can the - agent act now" is `verificationRequired` on the message response, so branch - on that and treat this status as a record of what has happened rather than - of what is permitted. - - This field reports the chat as it was when the turn started. The agent can - verify a cardholder during a turn, so a chat that was verified by the turn - you just made still reads `SENT` in that response and `VERIFIED` on the next - call. - - | Status | Description | - |--------|-------------| - | `PENDING` | No code has been sent on this chat yet. This is where every new chat starts. | - | `SENT` | A code has been sent to the cardholder and is outstanding. | - | `VERIFIED` | The cardholder sent the code as a message and the agent confirmed it. Account-reaching tools are available for as long as that answer is newer than the session lifetime, and the session token was issued on the message response to that turn. | - | `TRUSTED` | The cardholder is treated as present without an SMS code on this chat. Reserved for channels Grid can already attribute; a partner chat does not start here. Bounded by the same freshness window as `VERIFIED`. | - | `FAILED` | The last code was answered incorrectly or expired. The cardholder can send the code again, or ask the agent for a new one. | diff --git a/openapi/components/schemas/care/CareError.yaml b/openapi/components/schemas/care/CareError.yaml index e8aedd857..257ad96e7 100644 --- a/openapi/components/schemas/care/CareError.yaml +++ b/openapi/components/schemas/care/CareError.yaml @@ -23,7 +23,7 @@ properties: | Error Code | HTTP | Description | |------------|------|-------------| | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `chatId` path id beginning `Customer:` that is not well formed, or a chat that cannot be verified at all. | - | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused at the content boundary and no turn ran. `reason` carries the sentence to show them. | + | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused, for example because it is too long, and no turn ran. `reason` carries the sentence to show them. | | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified — that is signalled in a `200` body. | | `CARE_SESSION_EXPIRED` | 401 | The `X-Ls-Care-Session` token presented is malformed, or not a care session token at all; or a replay under an `Idempotency-Key` lacks the valid session token its original request required, in which case nothing from the record is returned. For a new message, a missing or expired token is never this error: the message verifies the cardholder again inside a `200` instead. Rarely, also returned when a verification lands on the chat at the same moment a message without a live token takes it away; send the message again. | | `FORBIDDEN` | 403 | Embedded care is not enabled for this platform. | @@ -33,11 +33,11 @@ properties: | `CONFLICT` | 409 | The `Idempotency-Key` was already used for a different message or a different chat — use a new key — or the chat has no cardholder bound to it. | | `CARE_CHAT_ENDED` | 409 | The chat has ended, whether the agent ended it or it was closed, and it accepts no further messages or openings; a replay of a turn that already completed is still answered. Open a new one by sending an empty message to the cardholder as `Customer:`. | | `CARE_NOT_CONFIGURED_FOR_PLATFORM` | 422 | The platform is entitled to embedded care but has no care programme configured. | - | `CARE_BUDGET_EXCEEDED` | 429 | This chat has reached its limit. It is not replaced by a new one: a replacement would be the limit cleared by asking again. | + | `CARE_BUDGET_EXCEEDED` | 429 | This chat has reached its limit. Opening a new chat for the cardholder does not clear it. | | `RATE_LIMITED` | 429 | The platform is calling this endpoint too often. Back off for the interval in the `Retry-After` response header. | | `INTERNAL_ERROR` | 500 | Unexpected server error. | | `CARE_TURN_FAILED` | 502 | The agent could not answer. Transient; the same `Idempotency-Key` may be sent again. | - | `CARE_VERIFICATION_UNAVAILABLE` | 502 | The SMS provider could not be reached. Distinct from a wrong code: the factor is down, so retry rather than asking the cardholder again. | + | `CARE_VERIFICATION_UNAVAILABLE` | 502 | The verification code could not be sent or checked right now. Distinct from a wrong code: the problem is temporary and not the cardholder's, so retry rather than asking them again. | | `SERVICE_UNAVAILABLE` | 503 | Embedded care is not available in this deployment, or the support agent is temporarily turned off. | | `CARE_TURN_TIMEOUT` | 504 | The turn is still running. Retry with the same `Idempotency-Key` to collect the answer. | enum: diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 7eaa304b2..2ec835957 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -112,23 +112,20 @@ tags: 1. **Open the chat.** `POST /care/chats/{chatId}/messages` with an empty `message`, addressed to the cardholder as `Customer:`. Returns the chat's id and the agent's opening line in `reply`, which is what - a chat window renders before anybody has typed. Sends no SMS, runs no - turn, needs no `Idempotency-Key`, bills nothing, and is safe to call - again: a widget that reloaded gets the same chat back, this time - without a `reply`. + a chat window renders before anybody has typed. Sends no SMS, needs + no `Idempotency-Key` and consumes none, and is safe to call again: a + widget that reloaded gets the same chat back, this time without a + `reply`. 2. **Send the cardholder's first message.** The same route with their text and an `Idempotency-Key` header. One turn runs synchronously and the agent's answer comes back in `reply`. On a chat that cannot yet - reach the account this call also texts the verification code and says - so in the same body: `verificationRequired: true`, with - `destinationMasked`, `alreadySent` and `codeLength`, and the agent's - `reply` asks the cardholder for the code. - 3. **The cardholder sends the code as an ordinary message.** The agent - checks it with the SMS provider, and the response to that turn - carries `sessionToken` and `expiresAt`. Send that token as the - `X-Ls-Care-Session` header on every message from then on. A code that - does not match is answered in `reply`, and the cardholder can simply - send it again. + reach the account this call also texts the cardholder a code, and the + agent's `reply` asks for it. + 3. **The cardholder sends the code as an ordinary message.** When it is + correct, the response to that turn carries `sessionToken` and + `expiresAt`. Send that token as the `X-Ls-Care-Session` header on + every message from then on. A code that does not match is answered in + `reply`, and the cardholder can simply send it again. 4. **Close the chat** with `DELETE /care/chats/{chatId}` when the cardholder is done. Every response also carries `chatEnded`, and the agent can end the chat itself: when `chatEnded` is true, the closing @@ -137,22 +134,30 @@ tags: `409 CARE_CHAT_ENDED`; address the cardholder as `Customer:` to open a new chat. + **Render `redactedCustomerMessage` if present, then `reply` if + present.** That one rule covers an opening, a resume and a turn. + **A cardholder whose code never arrived asks the agent for another one.** The agent sends a new code to the number on file and says so in its `reply`. There is no separate route for it. - **Render `displayMessage` if present, then `reply` if present.** That one - rule covers an opening, a resume and a turn. + **The verification fields are optional hints.** `verificationRequired` + and `codeLength` let an app show 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. A partner can ignore them, since the agent asks for the + code in its reply and says where it went, and the app still sends the + code as an ordinary message. Step 1 is optional. A message addressed to a `Customer:` id opens that cardholder's chat if they have none, so a partner whose cardholder has just typed something spends one call rather than two. - A cardholder who has not verified yet is never a `401`. That state is - reported inside a `200`, because the first message from every new - cardholder is unverified and a `401` there would be indistinguishable - from a bad credential. On these endpoints a `401` means the platform's - Grid credential is wrong, or the session token presented is malformed. + A cardholder who has not verified yet is never a `401`, because the + first message from every new cardholder is unverified and a `401` there + would be indistinguishable from a bad credential. On these endpoints a + `401` means the platform's Grid credential is wrong, the session token + presented is malformed, or a replay lacks the session token its original + request required. **Verification expires, and a lost session is recoverable in the chat.** A chat reaches the cardholder's account only while its @@ -160,27 +165,27 @@ tags: default deployment, which is also how long a session token lives. Past that window a message gets a fresh code, and the agent asks the cardholder to send it back inside the conversation, which hands the - partner the next session token. A message sent with no + partner the next session token. 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 taken away before the turn runs, a new code is sent, and + verification is cancelled before the turn runs, a new code is sent, and the agent asks for it. So a client that lost its token, or held one too long, simply keeps sending messages. Only a token that is malformed, or that was minted for another platform, chat or cardholder, is refused. Two things an integration has to get right: - - **Render `displayMessage`, never your own copy of what was typed.** - Grid redacts the cardholder's text on the way in, and `displayMessage` - is that redacted echo. A UI that renders its own input box instead has - put a card number the cardholder pasted back on their screen unmasked, - and into every log the transcript reaches. + - **Render `redactedCustomerMessage`, never your own copy of what was + typed.** Grid masks card numbers and other sensitive digit strings in + it. A UI that renders its own copy of the input instead puts a card + number the cardholder pasted back on their screen unmasked, and into + your logs. - **The session token is returned once and is never re-readable.** No - endpoint hands it back, there is no revocation, and it is short-lived - by design. Hold it for the life of the cardholder's session. It - arrives on the message response to the turn in which the cardholder - sent their code, and on no other. Losing it costs the cardholder a new - code, never access to the chat. + endpoint hands it back, it cannot be revoked, and it is short-lived by + design. Hold it for the life of the cardholder's session. It arrives + on the message response to the turn in which the cardholder sent their + code, and on no other. Losing it costs the cardholder a new code, never + access to the chat. Some things are absent by construction, so no integration can come to depend on them: nothing returns a transcript or an earlier turn's reply @@ -189,12 +194,11 @@ tags: session token its original request required and never carries a `sessionToken`, so a lost token is recovered by sending the cardholder's next message as a new message, which verifies them again), nothing sets - a verification - state, and nothing changes the number a code is sent to. + a verification state, and nothing changes the number a code is sent to. - Errors from these endpoints carry `code` and `reason` only — see the - `CareError` schema, which differs from the `Error4xx` schemas used - elsewhere in this specification. + Errors from these endpoints carry `code` and `reason` only (see the + `CareError` schema), unlike the `Error4xx` schemas used elsewhere in this + specification. servers: - url: https://api.lightspark.com/grid/2025-10-13 description: Production server diff --git a/openapi/paths/care/care_chats_{chatId}.yaml b/openapi/paths/care/care_chats_{chatId}.yaml index 01bd81df8..4e60d91d3 100644 --- a/openapi/paths/care/care_chats_{chatId}.yaml +++ b/openapi/paths/care/care_chats_{chatId}.yaml @@ -16,14 +16,12 @@ delete: with `CARE_CHAT_ENDED`, and the cardholder starts a new chat by sending another message. - No session token is required, even on a verified chat. Ending a chat is - the one thing the second factor is exempt from: it reaches nothing but the - chat's own closing timestamp, and requiring a live session would mean a - partner whose cardholder walked away could not close the chat without - texting them another code first. Ownership of the chat by the calling - platform is the whole bound. + No session token is required, even on a verified chat. Ending a chat + touches no account data, and requiring a live session would mean a partner + whose cardholder walked away could not close the chat without texting them + another code first. The chat only has to belong to the calling platform. - Closing a chat that is already closed is not an error — that is the state + Closing a chat that is already closed is not an error: that is the state the caller asked for. operationId: endCareChat tags: diff --git a/openapi/paths/care/care_chats_{chatId}_messages.yaml b/openapi/paths/care/care_chats_{chatId}_messages.yaml index 9ac8fef14..1b0c649b8 100644 --- a/openapi/paths/care/care_chats_{chatId}_messages.yaml +++ b/openapi/paths/care/care_chats_{chatId}_messages.yaml @@ -15,45 +15,52 @@ parameters: post: summary: Send a care chat message description: | - 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 + 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 reads a transcript, so a + 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 recorded answer with - `replayed: true`. **A replay requires the same session token its original + 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 nothing from - the record. 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 turn that failed does not pin its key: the same key may be sent again - and will run a fresh turn. + 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 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 nothing.** On a + **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`, `displayMessage` and `replayed` are absent on - both, because nothing was typed and no turn ran. Either way no code is - sent, no `Idempotency-Key` is needed or consumed, and nothing is billed, - 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:` that has ended: - it answers `409 CARE_CHAT_ENDED`, and the recovery is to address the - cardholder as `Customer:`, which opens a new chat. + 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:` that has ended: it answers + `409 CARE_CHAT_ENDED`, and the recovery is to address the cardholder as + `Customer:`, 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 @@ -63,31 +70,24 @@ post: what this call did, so a replay of that turn also says true. When it is true, stop sending to that chat. - **Verification is signalled in this `200` body, never as a `401`.** Every - new cardholder's first message is unverified, so a `401` there would make - the normal case indistinguishable from a bad credential. A chat that - cannot yet reach the account answers `200` with - `verificationRequired: true`, and the first message with text is what - sends the code — `destinationMasked`, `alreadySent` and, where readable, - `codeLength` come back with it, and the agent's own sentence in `reply` - asks the cardholder for the code. Later messages on the same chat find the - code already outstanding and send nothing, reporting `alreadySent: true`. - - **Render `displayMessage` if present, then `reply` if present.** That one - rule covers every call: an opening, a resume and a turn. Grid redacts the - cardholder's text on the way in, so a card number they pasted comes back - masked, and `displayMessage` is that redacted echo. A UI that echoes its - own input box instead has put an unmasked PAN in the transcript, on the - screen and in whatever logs the transcript reaches. + **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.** The - agent checks the code with the SMS provider, and the chat comes out of - that turn able to reach the account. 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. + 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. @@ -101,24 +101,20 @@ post: 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 chat no longer reaches the account, the header stops - being required, and the next message **sent without it** is treated as an - unverified one: a fresh code goes out, the body carries - `verificationRequired: true` with `destinationMasked`, `alreadySent` and - `codeLength`, and the agent's own `reply` asks the cardholder to send the - code. The turn still runs and still answers. + 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 message sent with no `X-Ls-Care-Session` header, or with a + 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 chat's verification is taken away 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 body - carries `verificationRequired: true` with `destinationMasked`, - `alreadySent` and `codeLength`, the agent asks the cardholder for the - code, 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. + 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 @@ -128,13 +124,11 @@ post: widget still holding a dead token can resume with one and be told where verification stands without texting anybody. - Everything in the response describes the chat as of the moment the turn - started, `verificationStatus` included. The agent can verify a cardholder - during a turn, so a chat that this very call verified still reads - unverified here and verified on the next call. For the same reason a chat - whose verification has gone stale reports `verificationStatus: VERIFIED` - beside `verificationRequired: true`. Branch on `verificationRequired`, - never on `verificationStatus`. + 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`. operationId: sendCareChatMessage tags: - Embedded Care @@ -148,11 +142,11 @@ post: A unique identifier for this turn, up to 255 characters. **Required whenever `message` is non-empty**; omitting it there is rejected with `INVALID_INPUT`. A retry carrying the same key returns the first - call's answer with `replayed: true`; aiming the same key at a - different message, a different chat or a different cardholder is - rejected with `409 CONFLICT`. An opening (empty `message`) neither - needs nor consumes a key — one sent anyway is ignored, so that the - partner's first real message can still use it. + call's answer with the same `id` (subject to the session rule for + replays); aiming the same key at a different message, a different chat + or a different cardholder is rejected with `409 CONFLICT`. An opening + (empty `message`) neither needs nor consumes a key: one sent anyway is + ignored, so that the partner's first real message can still use it. schema: type: string maxLength: 255 @@ -165,11 +159,11 @@ post: cardholder sent their verification code. Send it on every message while the chat's verification is current. A message without it, or with one past its expiry, is not refused: the chat's verification is - taken away before the turn runs and a new code is sent, so the + cancelled before the turn runs and a new code is sent, so the cardholder verifies again in the conversation. The exception is a replay under an `Idempotency-Key` whose original request required a session: it must carry a valid token, or it is refused with - `CARE_SESSION_EXPIRED` and returns nothing from the record. A token that is sent is + `CARE_SESSION_EXPIRED` and returns no reply. A token that is sent is always checked for what it names: a malformed one is refused with `CARE_SESSION_EXPIRED`, one minted for another chat or another cardholder is `NOT_FOUND`, and one minted for another platform is @@ -217,9 +211,8 @@ post: 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 at - the content boundary — no turn ran, and `reason` is written to be - shown to them. + `CARE_MESSAGE_REJECTED` when the cardholder's message was refused + before any turn ran; `reason` is written to be shown to them. content: application/json: schema: @@ -229,8 +222,8 @@ post: `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; nothing - from the record is returned. Otherwise a missing or expired session + 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. @@ -287,9 +280,9 @@ post: $ref: ../../components/schemas/care/CareError.yaml '429': description: >- - `CARE_BUDGET_EXCEEDED` when this chat has reached its limit. The chat - is refused rather than replaced: opening a fresh one would be the - limit cleared by asking again. `RATE_LIMITED` when the platform is + `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. content: From eae2f6cc7495fd50d036bc5a0ce5ecfa5a27f889 Mon Sep 17 00:00:00 2001 From: kphurley7 Date: Thu, 17 Sep 2026 11:33:56 -0500 Subject: [PATCH 10/11] docs(care): a retry of a running turn is a replay, and drop em dashes Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015njn7Fen4mg9NDX7AZQRTj --- mintlify/openapi.yaml | 26 ++++++++++--------- openapi.yaml | 26 ++++++++++--------- .../components/schemas/care/CareError.yaml | 10 +++---- openapi/paths/care/care_chats_{chatId}.yaml | 8 +++--- .../care/care_chats_{chatId}_messages.yaml | 8 +++--- 5 files changed, 42 insertions(+), 36 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index f07a11075..36a82734d 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -11117,7 +11117,9 @@ paths: 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 turn that + 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. @@ -11278,7 +11280,7 @@ paths: schema: $ref: '#/components/schemas/CareError' '409': - description: '`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 already completed; open a new one by sending a message addressed to the cardholder as `Customer:`, which finds no open chat and opens a fresh one.' + description: '`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:`, which finds no open chat and opens a fresh one.' content: application/json: schema: @@ -11314,7 +11316,7 @@ paths: schema: $ref: '#/components/schemas/CareError' '504': - description: '`CARE_TURN_TIMEOUT` when the turn is still running. The answer is not lost — retry with the same `Idempotency-Key` to collect it.' + description: '`CARE_TURN_TIMEOUT` when the turn is still running. The answer is not lost: retry with the same `Idempotency-Key` to collect it.' content: application/json: schema: @@ -11352,25 +11354,25 @@ paths: '204': description: The chat is closed. No content. '401': - description: '`UNAUTHORIZED` — the platform''s Grid credential is missing or wrong.' + description: '`UNAUTHORIZED`: the platform''s Grid credential is missing or wrong.' content: application/json: schema: $ref: '#/components/schemas/CareError' '403': - description: '`FORBIDDEN` — embedded care is not enabled for this platform.' + description: '`FORBIDDEN`: embedded care is not enabled for this platform.' content: application/json: schema: $ref: '#/components/schemas/CareError' '404': - description: '`NOT_FOUND` — no such chat for this platform. A chat that belongs to another platform, one that never existed, and a malformed chat id are one answer.' + description: '`NOT_FOUND`: no such chat for this platform. A chat that belongs to another platform, one that never existed, and a malformed chat id are one answer.' content: application/json: schema: $ref: '#/components/schemas/CareError' '429': - description: '`RATE_LIMITED` — the platform is calling this endpoint too often. Back off for the interval in the `Retry-After` header.' + description: '`RATE_LIMITED`: the platform is calling this endpoint too often. Back off for the interval in the `Retry-After` header.' content: application/json: schema: @@ -29257,7 +29259,7 @@ components: fields. Read the HTTP status from the response line and the human-readable text from `reason`. - Branch on `code`, never on `reason` — the text is written for a human + Branch on `code`, never on `reason`: the text is written for a human reading a log and may be reworded at any time. required: - code @@ -29273,14 +29275,14 @@ components: |------------|------|-------------| | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `chatId` path id beginning `Customer:` that is not well formed, or a chat that cannot be verified at all. | | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused, for example because it is too long, and no turn ran. `reason` carries the sentence to show them. | - | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified — that is signalled in a `200` body. | + | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified; that is signalled in a `200` body. | | `CARE_SESSION_EXPIRED` | 401 | The `X-Ls-Care-Session` token presented is malformed, or not a care session token at all; or a replay under an `Idempotency-Key` lacks the valid session token its original request required, in which case nothing from the record is returned. For a new message, a missing or expired token is never this error: the message verifies the cardholder again inside a `200` instead. Rarely, also returned when a verification lands on the chat at the same moment a message without a live token takes it away; send the message again. | | `FORBIDDEN` | 403 | Embedded care is not enabled for this platform. | | `CARE_SESSION_PLATFORM_MISMATCH` | 403 | The session token was minted for a different platform. Stop; retrying will not help. | | `NOT_FOUND` | 404 | No such chat for this platform. A chat belonging to another platform, a chat that never existed, a malformed chat id, and an `X-Ls-Care-Session` token minted for a different chat or a different cardholder are one answer. | | `CUSTOMER_NOT_FOUND` | 404 | No such cardholder for this platform. | - | `CONFLICT` | 409 | The `Idempotency-Key` was already used for a different message or a different chat — use a new key — or the chat has no cardholder bound to it. | - | `CARE_CHAT_ENDED` | 409 | The chat has ended, whether the agent ended it or it was closed, and it accepts no further messages or openings; a replay of a turn that already completed is still answered. Open a new one by sending an empty message to the cardholder as `Customer:`. | + | `CONFLICT` | 409 | The `Idempotency-Key` was already used for a different message or a different chat (use a new key), or the chat has no cardholder bound to it. | + | `CARE_CHAT_ENDED` | 409 | The chat has ended, whether the agent ended it or it was closed, and it accepts no further messages or openings; a replay of a turn that completed or is still running is still answered. Open a new one by sending an empty message to the cardholder as `Customer:`. | | `CARE_NOT_CONFIGURED_FOR_PLATFORM` | 422 | The platform is entitled to embedded care but has no care programme configured. | | `CARE_BUDGET_EXCEEDED` | 429 | This chat has reached its limit. Opening a new chat for the cardholder does not clear it. | | `RATE_LIMITED` | 429 | The platform is calling this endpoint too often. Back off for the interval in the `Retry-After` response header. | @@ -29311,7 +29313,7 @@ components: example: CARE_SESSION_EXPIRED reason: type: string - description: Human-readable explanation, for a log or a support ticket. Not for branching, and not always safe to show a cardholder verbatim — the exception is `CARE_MESSAGE_REJECTED`, whose `reason` is written to be shown. + description: Human-readable explanation, for a log or a support ticket. Not for branching, and not always safe to show a cardholder verbatim. The exception is `CARE_MESSAGE_REJECTED`, whose `reason` is written to be shown. example: The care session token is not valid. WebhookType: type: string diff --git a/openapi.yaml b/openapi.yaml index f07a11075..36a82734d 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -11117,7 +11117,9 @@ paths: 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 turn that + 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. @@ -11278,7 +11280,7 @@ paths: schema: $ref: '#/components/schemas/CareError' '409': - description: '`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 already completed; open a new one by sending a message addressed to the cardholder as `Customer:`, which finds no open chat and opens a fresh one.' + description: '`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:`, which finds no open chat and opens a fresh one.' content: application/json: schema: @@ -11314,7 +11316,7 @@ paths: schema: $ref: '#/components/schemas/CareError' '504': - description: '`CARE_TURN_TIMEOUT` when the turn is still running. The answer is not lost — retry with the same `Idempotency-Key` to collect it.' + description: '`CARE_TURN_TIMEOUT` when the turn is still running. The answer is not lost: retry with the same `Idempotency-Key` to collect it.' content: application/json: schema: @@ -11352,25 +11354,25 @@ paths: '204': description: The chat is closed. No content. '401': - description: '`UNAUTHORIZED` — the platform''s Grid credential is missing or wrong.' + description: '`UNAUTHORIZED`: the platform''s Grid credential is missing or wrong.' content: application/json: schema: $ref: '#/components/schemas/CareError' '403': - description: '`FORBIDDEN` — embedded care is not enabled for this platform.' + description: '`FORBIDDEN`: embedded care is not enabled for this platform.' content: application/json: schema: $ref: '#/components/schemas/CareError' '404': - description: '`NOT_FOUND` — no such chat for this platform. A chat that belongs to another platform, one that never existed, and a malformed chat id are one answer.' + description: '`NOT_FOUND`: no such chat for this platform. A chat that belongs to another platform, one that never existed, and a malformed chat id are one answer.' content: application/json: schema: $ref: '#/components/schemas/CareError' '429': - description: '`RATE_LIMITED` — the platform is calling this endpoint too often. Back off for the interval in the `Retry-After` header.' + description: '`RATE_LIMITED`: the platform is calling this endpoint too often. Back off for the interval in the `Retry-After` header.' content: application/json: schema: @@ -29257,7 +29259,7 @@ components: fields. Read the HTTP status from the response line and the human-readable text from `reason`. - Branch on `code`, never on `reason` — the text is written for a human + Branch on `code`, never on `reason`: the text is written for a human reading a log and may be reworded at any time. required: - code @@ -29273,14 +29275,14 @@ components: |------------|------|-------------| | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `chatId` path id beginning `Customer:` that is not well formed, or a chat that cannot be verified at all. | | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused, for example because it is too long, and no turn ran. `reason` carries the sentence to show them. | - | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified — that is signalled in a `200` body. | + | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified; that is signalled in a `200` body. | | `CARE_SESSION_EXPIRED` | 401 | The `X-Ls-Care-Session` token presented is malformed, or not a care session token at all; or a replay under an `Idempotency-Key` lacks the valid session token its original request required, in which case nothing from the record is returned. For a new message, a missing or expired token is never this error: the message verifies the cardholder again inside a `200` instead. Rarely, also returned when a verification lands on the chat at the same moment a message without a live token takes it away; send the message again. | | `FORBIDDEN` | 403 | Embedded care is not enabled for this platform. | | `CARE_SESSION_PLATFORM_MISMATCH` | 403 | The session token was minted for a different platform. Stop; retrying will not help. | | `NOT_FOUND` | 404 | No such chat for this platform. A chat belonging to another platform, a chat that never existed, a malformed chat id, and an `X-Ls-Care-Session` token minted for a different chat or a different cardholder are one answer. | | `CUSTOMER_NOT_FOUND` | 404 | No such cardholder for this platform. | - | `CONFLICT` | 409 | The `Idempotency-Key` was already used for a different message or a different chat — use a new key — or the chat has no cardholder bound to it. | - | `CARE_CHAT_ENDED` | 409 | The chat has ended, whether the agent ended it or it was closed, and it accepts no further messages or openings; a replay of a turn that already completed is still answered. Open a new one by sending an empty message to the cardholder as `Customer:`. | + | `CONFLICT` | 409 | The `Idempotency-Key` was already used for a different message or a different chat (use a new key), or the chat has no cardholder bound to it. | + | `CARE_CHAT_ENDED` | 409 | The chat has ended, whether the agent ended it or it was closed, and it accepts no further messages or openings; a replay of a turn that completed or is still running is still answered. Open a new one by sending an empty message to the cardholder as `Customer:`. | | `CARE_NOT_CONFIGURED_FOR_PLATFORM` | 422 | The platform is entitled to embedded care but has no care programme configured. | | `CARE_BUDGET_EXCEEDED` | 429 | This chat has reached its limit. Opening a new chat for the cardholder does not clear it. | | `RATE_LIMITED` | 429 | The platform is calling this endpoint too often. Back off for the interval in the `Retry-After` response header. | @@ -29311,7 +29313,7 @@ components: example: CARE_SESSION_EXPIRED reason: type: string - description: Human-readable explanation, for a log or a support ticket. Not for branching, and not always safe to show a cardholder verbatim — the exception is `CARE_MESSAGE_REJECTED`, whose `reason` is written to be shown. + description: Human-readable explanation, for a log or a support ticket. Not for branching, and not always safe to show a cardholder verbatim. The exception is `CARE_MESSAGE_REJECTED`, whose `reason` is written to be shown. example: The care session token is not valid. WebhookType: type: string diff --git a/openapi/components/schemas/care/CareError.yaml b/openapi/components/schemas/care/CareError.yaml index 257ad96e7..c34fa869d 100644 --- a/openapi/components/schemas/care/CareError.yaml +++ b/openapi/components/schemas/care/CareError.yaml @@ -8,7 +8,7 @@ description: | fields. Read the HTTP status from the response line and the human-readable text from `reason`. - Branch on `code`, never on `reason` — the text is written for a human + Branch on `code`, never on `reason`: the text is written for a human reading a log and may be reworded at any time. required: - code @@ -24,14 +24,14 @@ properties: |------------|------|-------------| | `INVALID_INPUT` | 400 | The request body or headers are malformed: an unknown field, a body that is not a JSON object, a missing or over-long `Idempotency-Key` on a message that carries text, a `chatId` path id beginning `Customer:` that is not well formed, or a chat that cannot be verified at all. | | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused, for example because it is too long, and no turn ran. `reason` carries the sentence to show them. | - | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified — that is signalled in a `200` body. | + | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified; that is signalled in a `200` body. | | `CARE_SESSION_EXPIRED` | 401 | The `X-Ls-Care-Session` token presented is malformed, or not a care session token at all; or a replay under an `Idempotency-Key` lacks the valid session token its original request required, in which case nothing from the record is returned. For a new message, a missing or expired token is never this error: the message verifies the cardholder again inside a `200` instead. Rarely, also returned when a verification lands on the chat at the same moment a message without a live token takes it away; send the message again. | | `FORBIDDEN` | 403 | Embedded care is not enabled for this platform. | | `CARE_SESSION_PLATFORM_MISMATCH` | 403 | The session token was minted for a different platform. Stop; retrying will not help. | | `NOT_FOUND` | 404 | No such chat for this platform. A chat belonging to another platform, a chat that never existed, a malformed chat id, and an `X-Ls-Care-Session` token minted for a different chat or a different cardholder are one answer. | | `CUSTOMER_NOT_FOUND` | 404 | No such cardholder for this platform. | - | `CONFLICT` | 409 | The `Idempotency-Key` was already used for a different message or a different chat — use a new key — or the chat has no cardholder bound to it. | - | `CARE_CHAT_ENDED` | 409 | The chat has ended, whether the agent ended it or it was closed, and it accepts no further messages or openings; a replay of a turn that already completed is still answered. Open a new one by sending an empty message to the cardholder as `Customer:`. | + | `CONFLICT` | 409 | The `Idempotency-Key` was already used for a different message or a different chat (use a new key), or the chat has no cardholder bound to it. | + | `CARE_CHAT_ENDED` | 409 | The chat has ended, whether the agent ended it or it was closed, and it accepts no further messages or openings; a replay of a turn that completed or is still running is still answered. Open a new one by sending an empty message to the cardholder as `Customer:`. | | `CARE_NOT_CONFIGURED_FOR_PLATFORM` | 422 | The platform is entitled to embedded care but has no care programme configured. | | `CARE_BUDGET_EXCEEDED` | 429 | This chat has reached its limit. Opening a new chat for the cardholder does not clear it. | | `RATE_LIMITED` | 429 | The platform is calling this endpoint too often. Back off for the interval in the `Retry-After` response header. | @@ -64,7 +64,7 @@ properties: type: string description: >- Human-readable explanation, for a log or a support ticket. Not for - branching, and not always safe to show a cardholder verbatim — the + branching, and not always safe to show a cardholder verbatim. The exception is `CARE_MESSAGE_REJECTED`, whose `reason` is written to be shown. example: The care session token is not valid. diff --git a/openapi/paths/care/care_chats_{chatId}.yaml b/openapi/paths/care/care_chats_{chatId}.yaml index 4e60d91d3..40f56bad8 100644 --- a/openapi/paths/care/care_chats_{chatId}.yaml +++ b/openapi/paths/care/care_chats_{chatId}.yaml @@ -32,20 +32,20 @@ delete: '204': description: The chat is closed. No content. '401': - description: '`UNAUTHORIZED` — the platform''s Grid credential is missing or wrong.' + description: '`UNAUTHORIZED`: the platform''s Grid credential is missing or wrong.' content: application/json: schema: $ref: ../../components/schemas/care/CareError.yaml '403': - description: '`FORBIDDEN` — embedded care is not enabled for this platform.' + description: '`FORBIDDEN`: embedded care is not enabled for this platform.' content: application/json: schema: $ref: ../../components/schemas/care/CareError.yaml '404': description: >- - `NOT_FOUND` — no such chat for this platform. A chat that belongs to + `NOT_FOUND`: no such chat for this platform. A chat that belongs to another platform, one that never existed, and a malformed chat id are one answer. content: @@ -54,7 +54,7 @@ delete: $ref: ../../components/schemas/care/CareError.yaml '429': description: >- - `RATE_LIMITED` — the platform is calling this endpoint too often. Back + `RATE_LIMITED`: the platform is calling this endpoint too often. Back off for the interval in the `Retry-After` header. content: application/json: diff --git a/openapi/paths/care/care_chats_{chatId}_messages.yaml b/openapi/paths/care/care_chats_{chatId}_messages.yaml index 1b0c649b8..86b842b88 100644 --- a/openapi/paths/care/care_chats_{chatId}_messages.yaml +++ b/openapi/paths/care/care_chats_{chatId}_messages.yaml @@ -44,7 +44,9 @@ post: 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 turn that + 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. @@ -262,7 +264,7 @@ post: 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 already completed; open a new one by sending a message + turn that completed or is still running; open a new one by sending a message addressed to the cardholder as `Customer:`, which finds no open chat and opens a fresh one. content: @@ -316,7 +318,7 @@ post: '504': description: >- `CARE_TURN_TIMEOUT` when the turn is still running. The answer is not - lost — retry with the same `Idempotency-Key` to collect it. + lost: retry with the same `Idempotency-Key` to collect it. content: application/json: schema: From 57904f0e4f97fd3089b0b62dc2da548479e15528 Mon Sep 17 00:00:00 2001 From: kphurley7 Date: Wed, 23 Sep 2026 10:35:29 -0700 Subject: [PATCH 11/11] docs(care): document the token permissions the chat endpoints require Sending a message needs VIEW and MANAGE, ending a chat needs VIEW, and each endpoint's 403 names both causes: a missing permission, or embedded care not enabled for the platform. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_015njn7Fen4mg9NDX7AZQRTj --- mintlify/openapi.yaml | 12 +++++++++--- openapi.yaml | 12 +++++++++--- openapi/components/schemas/care/CareError.yaml | 2 +- openapi/paths/care/care_chats_{chatId}.yaml | 6 +++++- openapi/paths/care/care_chats_{chatId}_messages.yaml | 7 ++++++- 5 files changed, 30 insertions(+), 9 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 36a82734d..16244a796 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -11204,6 +11204,10 @@ paths: 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. operationId: sendCareChatMessage tags: - Embedded Care @@ -11268,7 +11272,7 @@ paths: schema: $ref: '#/components/schemas/CareError' '403': - description: '`FORBIDDEN` when embedded care is not enabled for this platform, or `CARE_SESSION_PLATFORM_MISMATCH` when the session token was minted for another platform.' + description: '`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.' content: application/json: schema: @@ -11345,6 +11349,8 @@ paths: Closing a chat that is already closed is not an error: that is the state the caller asked for. + + Requires a token with the `VIEW` permission. operationId: endCareChat tags: - Embedded Care @@ -11360,7 +11366,7 @@ paths: schema: $ref: '#/components/schemas/CareError' '403': - description: '`FORBIDDEN`: embedded care is not enabled for this platform.' + description: '`FORBIDDEN`: the token lacks the `VIEW` permission, or embedded care is not enabled for this platform.' content: application/json: schema: @@ -29277,7 +29283,7 @@ components: | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused, for example because it is too long, and no turn ran. `reason` carries the sentence to show them. | | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified; that is signalled in a `200` body. | | `CARE_SESSION_EXPIRED` | 401 | The `X-Ls-Care-Session` token presented is malformed, or not a care session token at all; or a replay under an `Idempotency-Key` lacks the valid session token its original request required, in which case nothing from the record is returned. For a new message, a missing or expired token is never this error: the message verifies the cardholder again inside a `200` instead. Rarely, also returned when a verification lands on the chat at the same moment a message without a live token takes it away; send the message again. | - | `FORBIDDEN` | 403 | Embedded care is not enabled for this platform. | + | `FORBIDDEN` | 403 | The token lacks a permission the endpoint requires, or embedded care is not enabled for this platform. | | `CARE_SESSION_PLATFORM_MISMATCH` | 403 | The session token was minted for a different platform. Stop; retrying will not help. | | `NOT_FOUND` | 404 | No such chat for this platform. A chat belonging to another platform, a chat that never existed, a malformed chat id, and an `X-Ls-Care-Session` token minted for a different chat or a different cardholder are one answer. | | `CUSTOMER_NOT_FOUND` | 404 | No such cardholder for this platform. | diff --git a/openapi.yaml b/openapi.yaml index 36a82734d..16244a796 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -11204,6 +11204,10 @@ paths: 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. operationId: sendCareChatMessage tags: - Embedded Care @@ -11268,7 +11272,7 @@ paths: schema: $ref: '#/components/schemas/CareError' '403': - description: '`FORBIDDEN` when embedded care is not enabled for this platform, or `CARE_SESSION_PLATFORM_MISMATCH` when the session token was minted for another platform.' + description: '`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.' content: application/json: schema: @@ -11345,6 +11349,8 @@ paths: Closing a chat that is already closed is not an error: that is the state the caller asked for. + + Requires a token with the `VIEW` permission. operationId: endCareChat tags: - Embedded Care @@ -11360,7 +11366,7 @@ paths: schema: $ref: '#/components/schemas/CareError' '403': - description: '`FORBIDDEN`: embedded care is not enabled for this platform.' + description: '`FORBIDDEN`: the token lacks the `VIEW` permission, or embedded care is not enabled for this platform.' content: application/json: schema: @@ -29277,7 +29283,7 @@ components: | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused, for example because it is too long, and no turn ran. `reason` carries the sentence to show them. | | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified; that is signalled in a `200` body. | | `CARE_SESSION_EXPIRED` | 401 | The `X-Ls-Care-Session` token presented is malformed, or not a care session token at all; or a replay under an `Idempotency-Key` lacks the valid session token its original request required, in which case nothing from the record is returned. For a new message, a missing or expired token is never this error: the message verifies the cardholder again inside a `200` instead. Rarely, also returned when a verification lands on the chat at the same moment a message without a live token takes it away; send the message again. | - | `FORBIDDEN` | 403 | Embedded care is not enabled for this platform. | + | `FORBIDDEN` | 403 | The token lacks a permission the endpoint requires, or embedded care is not enabled for this platform. | | `CARE_SESSION_PLATFORM_MISMATCH` | 403 | The session token was minted for a different platform. Stop; retrying will not help. | | `NOT_FOUND` | 404 | No such chat for this platform. A chat belonging to another platform, a chat that never existed, a malformed chat id, and an `X-Ls-Care-Session` token minted for a different chat or a different cardholder are one answer. | | `CUSTOMER_NOT_FOUND` | 404 | No such cardholder for this platform. | diff --git a/openapi/components/schemas/care/CareError.yaml b/openapi/components/schemas/care/CareError.yaml index c34fa869d..4fb6ed24d 100644 --- a/openapi/components/schemas/care/CareError.yaml +++ b/openapi/components/schemas/care/CareError.yaml @@ -26,7 +26,7 @@ properties: | `CARE_MESSAGE_REJECTED` | 400 | The cardholder's message was refused, for example because it is too long, and no turn ran. `reason` carries the sentence to show them. | | `UNAUTHORIZED` | 401 | The platform's Grid credential is missing or wrong. On this surface a `401` never means the cardholder is unverified; that is signalled in a `200` body. | | `CARE_SESSION_EXPIRED` | 401 | The `X-Ls-Care-Session` token presented is malformed, or not a care session token at all; or a replay under an `Idempotency-Key` lacks the valid session token its original request required, in which case nothing from the record is returned. For a new message, a missing or expired token is never this error: the message verifies the cardholder again inside a `200` instead. Rarely, also returned when a verification lands on the chat at the same moment a message without a live token takes it away; send the message again. | - | `FORBIDDEN` | 403 | Embedded care is not enabled for this platform. | + | `FORBIDDEN` | 403 | The token lacks a permission the endpoint requires, or embedded care is not enabled for this platform. | | `CARE_SESSION_PLATFORM_MISMATCH` | 403 | The session token was minted for a different platform. Stop; retrying will not help. | | `NOT_FOUND` | 404 | No such chat for this platform. A chat belonging to another platform, a chat that never existed, a malformed chat id, and an `X-Ls-Care-Session` token minted for a different chat or a different cardholder are one answer. | | `CUSTOMER_NOT_FOUND` | 404 | No such cardholder for this platform. | diff --git a/openapi/paths/care/care_chats_{chatId}.yaml b/openapi/paths/care/care_chats_{chatId}.yaml index 40f56bad8..7c3070e53 100644 --- a/openapi/paths/care/care_chats_{chatId}.yaml +++ b/openapi/paths/care/care_chats_{chatId}.yaml @@ -23,6 +23,8 @@ delete: Closing a chat that is already closed is not an error: that is the state the caller asked for. + + Requires a token with the `VIEW` permission. operationId: endCareChat tags: - Embedded Care @@ -38,7 +40,9 @@ delete: schema: $ref: ../../components/schemas/care/CareError.yaml '403': - description: '`FORBIDDEN`: embedded care is not enabled for this platform.' + description: >- + `FORBIDDEN`: the token lacks the `VIEW` permission, or embedded care is + not enabled for this platform. content: application/json: schema: diff --git a/openapi/paths/care/care_chats_{chatId}_messages.yaml b/openapi/paths/care/care_chats_{chatId}_messages.yaml index 86b842b88..6c0b45f58 100644 --- a/openapi/paths/care/care_chats_{chatId}_messages.yaml +++ b/openapi/paths/care/care_chats_{chatId}_messages.yaml @@ -131,6 +131,10 @@ post: 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. operationId: sendCareChatMessage tags: - Embedded Care @@ -238,7 +242,8 @@ post: $ref: ../../components/schemas/care/CareError.yaml '403': description: >- - `FORBIDDEN` when embedded care is not enabled for this platform, or + `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. content: