Skip to main content
Send Synap the turns your user and your agent exchange. Synap keeps them as the conversation’s short-term context and turns them into long-term memory in the background, so your agent can pick up where it left off and remember the person next time. Which call you use depends on when you have the content: turn by turn while the conversation runs, a few turns at a time, or the whole conversation after it ends.

Choose a call

Use one path per conversation. Every path above already turns the content into long-term memory, so sending the same conversation down two of them makes Synap extract it twice. Send each conversation once explains the cases.

What to send

  • Both sides of the conversation. Record the user’s turn and your agent’s reply. Together they are the conversation Synap serves back as short-term context and later extracts memories from; a reply without its question, or a question without its answer, loses half of that.
  • Two roles only: user and assistant. There is no system role. The Python SDK raises ValueError for any other role before sending, and the server refuses it with HTTP 422 (InvalidInputError). Your system prompt is instructions you control, not something the person said, so leave it out.
  • Not tool calls or reasoning. Those have their own calls; see Traces and tool calls.
  • The person, on every call. Send user_id every time. On a B2B instance also send customer_id on every call; without it the server refuses the turn (HTTP 400, InvalidInputError, on the REST calls). On a B2C instance send user_id only: a customer_id is refused, usually by the SDK before the request leaves your process. See B2C vs B2B.

Record live turns

Record each turn as soon as you have it: the user’s message when it arrives, and the assistant’s reply after your agent produces it. Use one conversation_id for every turn of a conversation. record_message requires it to be a UUID and raises InvalidConversationIdError (a subclass of InvalidInputError) for anything else, before sending.
record_message returns message_id, conversation_id, session_id and recorded_at once the server has stored the turn.

Over the gRPC stream

If your agent keeps a stream open with instance.listen for real-time anticipation, send the turns on that stream with instance.send_message instead. Events with event_type user_message or assistant_message are stored in the conversation the same way record_message stores them. Set role and event_type together: the defaults are "user" and "user_message", so the reply needs both changed.
Three differences from record_message matter:
  • send_message raises ListeningNotActiveError if listen() has not been called.
  • It returns once the event is on the stream. The server stores the turn moments later, not before the call returns.
  • The SDK does not check that the id is a UUID; the server converts any other string into a UUID derived from it. Use a UUID anyway, because conversation.context.fetch and get_context_for_prompt only accept UUIDs, and you need the same id to read the conversation back.
Pick one of the two calls for a given conversation. A turn sent both ways is stored twice.

Record several turns at once

When you buffer turns, for example writing the user’s message and the reply together after each exchange, or flushing a queue from a worker, send them in one call with record_messages_batch. Each message takes the same fields as record_message.
The server stores each message on its own, in order. A message that fails does not stop the others and does not raise: it is counted in failed and described in errors, so check them. The response also carries total, succeeded and results. Problems the SDK can see before sending, such as a conversation id that is not a UUID, still raise for the whole batch. For older conversations you are loading in bulk, use Import existing history instead.

Push a finished transcript

When Synap had no part in the conversation while it ran, for example a voice call handled by your telephony stack or a support chat you process after it closes, push the whole thing at the end with ingest_transcript. One call records every turn as a conversation, queues extraction and starts a summary of the conversation, then returns right away with an ingestion_id.
What to know about each part:
  • conversation_id can be any string. Unlike record_message, the SDK does not require a UUID. The server converts a non-UUID string into a UUID derived from it, so the same string always maps to the same conversation. The response returns both: conversation_id is the UUID form, which is the one to pass to conversation.context calls, and external_conversation_id is the string you sent.
  • transcript is a list of turns or a string. Prefer the list: each turn keeps its own timestamp, a speaker display label and its own metadata. A string is split on lines that start with a label (User:, Assistant:, Human:, AI:, System: or Bot:); User and Human become user turns and every other label becomes an assistant turn. Typed turns accept user and assistant only. A transcript can hold up to 2,000 turns.
  • Send the times. ended_at, started_at or per-turn timestamps tell Synap when the conversation happened, which is what dates its memories and orders them against newer ones. When none is present, the response carries a warning saying so. Timestamps without a timezone are read as UTC.
  • analysis is your own JSON about the call (up to 64 KB), such as a disposition or sentiment from your platform. Synap stores it as is, returns it in conversation-summary fetches, and gives it to extraction as hints. Memories still have to come from what was said in the transcript.
  • conversation_type is a free label up to 64 characters, such as "voice" or "chat". metadata (up to 64 KB) is stored on the conversation.
Python exposes the response’s warnings list through resp.raw["warnings"]; in TypeScript it is resp.warnings.

Sending the same conversation_id again

A transcript is never replaced, so give every call its own conversation_id. The SDKs still define TranscriptConflictError for a conflicting transcript, but the current server accepts the push as described above instead of refusing it.

Import a conversation from another system

To bring in one conversation that happened somewhere else, such as a chat export from a previous tool, send it as a document with memories.create and document_type="ai-chat-conversation" (the default for memories.create). Put one turn per line, starting with User: or Assistant:, so Synap can split a long conversation at turn boundaries. Set document_created_at to when the conversation took place, so its memories are dated correctly.
This path only produces long-term memory. No conversation is recorded, so the content does not appear in conversation context or in conversation-summary fetches. When that matters, use ingest_transcript. For many conversations at once, use memories.batch_create as described in Import existing history.

What happens to recorded turns

Short-term context: right away

record_message and record_messages_batch return only after the server has stored the turn, so the next fetch of that conversation’s context includes it, from any process. The SDK also keeps a local copy of the turns it sent and adds them to the next context fetch in the same process. That covers send_message, whose turns reach the server moments after the call returns. This read-your-writes behavior is on by default in both SDKs.

Long-term memory: in the background

Synap does not extract memories from each turn the moment you record it. In the background, as the conversation grows and after it goes quiet or ends, Synap sends the recorded turns through the same extraction pipeline that memories.create uses. You do not call memories.create for turns you recorded. Context, end to end follows a message through this, and Context compaction covers the compacted context you can read back while the conversation runs. A transcript pushed with ingest_transcript is extracted once, from the transcript itself. Synap does not extract that conversation’s turns a second time later. The ingestion_id it returns is the handle for memories.status and memories.wait_for_completion; record_message returns no ingestion_id, because nothing is extracted at that moment.

Send each conversation once

  • Recorded turns plus memories.create of the same text. The turns are already on their way to long-term memory. A memories.create call with the assembled conversation is a separate document, and the duplicate check only catches identical text, so Synap extracts the same content twice. Use memories.create for content that is not a recorded turn.
  • Recorded turns plus ingest_transcript under the same id. Synap accepts the transcript and extracts it again, leaving the recorded turns in place. Do this only when the transcript adds something the live turns lack, such as analysis, start and end times, or a corrected version from your speech system.

Deeper scope ladders

On the default ladder, customer_id and user_id say everything needed. If your scope ladder has a level between customer and user, pass scope_path, naming every level down to the one the conversation belongs to:
  • record_message and ingest_transcript take scope_path, and each message in record_messages_batch can carry its own scope_path.
  • memories.create takes the same map under the name scope.
  • send_message cannot carry a path. Use record_message for a conversation that needs one.
For a recorded conversation, sending the path once is enough: the server fills in a missing path and never overwrites one it already stored.

Optional fields

Common mistakes

  • A new conversation_id on every turn. Each turn then becomes its own one-message conversation, and short-term context never holds more than that turn. Generate the id once per conversation and reuse it.
  • A non-UUID id on the live path. record_message rejects it outright. On send_message it is accepted but stored under a derived UUID, which you cannot pass to conversation.context.fetch.
  • customer_id on B2C, or none on B2B. Both are refused. Check which kind your instance is in B2C vs B2B.
  • Recording system messages or tool output as turns. Only user and assistant are accepted. Send tool activity with the calls in Traces and tool calls.
  • Sending a conversation twice. Recorded turns plus memories.create, or a live conversation plus a transcript of it, extracts the content twice.
  • Reusing a call’s id for the next call. The second transcript is extracted, but its turns are not recorded as a conversation (turns_recorded=0), and its analysis, type and times replace the first call’s on the conversation. Give each call its own id.
  • Recording more turns after pushing a transcript. Synap treats a transcript-pushed conversation as finished and does not extract later turns of it into long-term memory. Start a new conversation instead.
  • Ignoring batch results. record_messages_batch does not raise for a message that failed. Check failed and errors.
  • Transcripts without times. Without ended_at, started_at or turn timestamps, Synap cannot tell when the conversation happened.