Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .stainless/stainless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,17 @@ 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_error: "#/components/schemas/CareError"

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
Expand Down
557 changes: 557 additions & 0 deletions mintlify/openapi.yaml

Large diffs are not rendered by default.

557 changes: 557 additions & 0 deletions openapi.yaml

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions openapi/components/schemas/care/CareChatMessageRequest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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`.
Comment thread
kphurley7 marked this conversation as resolved.
Comment thread
kphurley7 marked this conversation as resolved.
additionalProperties: false
properties:
message:
type: string
description: >-
What the cardholder typed, verbatim and unredacted. Send it exactly as
they entered it: Grid masks card numbers and other sensitive digit
strings and returns the result as `redactedCustomerMessage`, which is
what your UI must render.


Empty, absent, or whitespace-only opens the chat (or resumes an open
one) and runs no turn: no agent reply to a message, no SMS, and no
`Idempotency-Key` needed or consumed. A non-empty value runs exactly one
turn synchronously and requires an `Idempotency-Key` header.
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
142 changes: 142 additions & 0 deletions openapi/components/schemas/care/CareChatMessageResponse.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
type: object
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
- verificationRequired
- chatEnded
properties:
chatId:
type: string
description: >-
The chat this call ran on. When the request addressed a cardholder
(`Customer:<uuid>` in the path) this is the chat that was found or
opened for them. Use it for every later call on this conversation:
later messages and closing the chat.
example: CareChat:019542f5-b3e7-1d02-0000-000000000030
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:<uuid>`, which opens a new chat.
example: false
verificationRequired:
type: boolean
description: >-
An optional hint: whether the cardholder still has to verify before the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you imagine the chat UX behaving when the verificationRequired response is true?

Does it mean the next message must be only the code, or do we use regex / AI to extract the code from the next message?

I'm having a hard time imagining what additional logic I would add if I were building a chat UI.

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 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
redactedCustomerMessage:
type: string
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` 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?
codeLength:
type: integer
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. When the code is correct, the chat comes out of that
turn able to reach the account, and this field is how the partner
receives the credential that turn earned. It is the only place a session
token is ever issued. A later message sent without it cancels the
verification and asks the cardholder for a new code.


Send it as the `X-Ls-Care-Session` header from the next call onwards.
It is returned once and never re-readable, it cannot be revoked, and it
is bound to this platform, this chat and this cardholder. Never log it,
never put it in a URL, and never store it past `expiresAt`.


One session is issued per verification rather than per message, so an
ordinary turn on a chat that is already verified carries no
`sessionToken`. The one you are already holding is still the right one.


A replay under the same `Idempotency-Key` never returns a
`sessionToken`, even if the original response carried one, and a replay
of a turn that required a session needs that session itself. So if the
response carrying the token was lost, you cannot recover it or replay an
account turn: send the cardholder's next message as a new message
without a token. The verification is cancelled, a new code is sent, and
the agent asks for it. A replay of the turn in which the cardholder sent
their code needs no token and returns the original reply without one.
example: eyJwbGF0Zm9ybV9pZCI6IjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAwMSJ9.aGVsbG8.c2lnbmF0dXJl
expiresAt:
type: string
format: date-time
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'
70 changes: 70 additions & 0 deletions openapi/components/schemas/care/CareError.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
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 `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. |
| `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 completed or is still running is still answered. Open a new one by sending an empty message to the cardholder as `Customer:<uuid>`. |
| `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. |
| `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 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:
- INVALID_INPUT
- CARE_MESSAGE_REJECTED
- 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 token is not valid.
Loading
Loading