Skip to main content
This is the canonical end-to-end tutorial: read it top-to-bottom. It assumes you’ve finished the Quickstart and want to wire Synap into a real application.If you only need a snippet for a specific framework (Flask, Next.js, Django) or LLM provider (Anthropic, Vercel AI SDK), open Setup → Integration and copy the relevant tab instead.Prefer to explore the SDK in a browser before writing code? Use the live playground.Working in JavaScript or TypeScript? This walkthrough is Python and FastAPI end to end. For the same loop in Node, use the Express tab in Setup → Integration, which is complete and runnable, and read this page for the reasoning behind each step.

Prerequisites

Before you begin, make sure you have:
  • Python 3.11+ installed on your machine (this tutorial is Python; see the note above for the Node equivalent)
  • A Synap account with access to the Dashboard
  • An instance created in the Dashboard (see Quickstart if you haven’t done this yet). For best results, upload a Use-Case Markdown file when creating your instance; see Use-Case Markdown for the template and authoring guide.
  • An OpenAI API key (or any LLM provider; we use OpenAI in this tutorial for simplicity). No paid key yet? Use Google Gemini’s free tier; see the Gemini snippet in Setup → Integration.
This tutorial assumes basic familiarity with Python async/await. If you are new to async Python, check out the asyncio documentation first.

1

Set Up Your Project

Create a new directory for your project and install the required dependencies:
Install the SDK and supporting libraries:
Your project will have the following structure:
2

Configure Your Environment

Create a .env file with your credentials. You will need two values:
  • SYNAP_API_KEY: The API key generated for your instance (format: synap_<random>). Generate one from the Dashboard: open your instance, click Generate API Key, and copy the key; it is shown only once.
  • SYNAP_INSTANCE_ID: The instance id shown alongside the key in the Dashboard (format: inst_ plus 16 hex characters). Optional, since initialize() resolves it from the API key, but the Dashboard gives you both together so you may as well paste both.
  • OPENAI_API_KEY: Your OpenAI API key
.env
Set the instance id as an environment variable, never as a constructor argument. SYNAP_INSTANCE_ID records which instance you are on and leaves the SDK keyed on your credential. Passing instance_id= to MaximemSynapSDK(...) makes the id the identity instead, so a second key used under it is silently discarded and key rotation stops taking effect. See Singleton Pattern.
Never commit .env files to version control. Add .env to your .gitignore immediately. In production, use a secrets manager (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault) instead of environment files.
3

Initialize the SDK

Create a module that manages the SDK lifecycle, imported by your application. The Python tab uses FastAPI; the JavaScript tabs use Express.
Key points about this setup:
  • sdk.instance.listen() opens one long-lived stream for the whole process. Your agent reports each turn on it, and Synap pushes anticipated context back so retrieval resolves locally. This is the Agent Integration.
  • cache_backend="sqlite" enables local caching for faster repeated retrievals.
  • log_level="INFO" is appropriate for development. Switch to "WARNING" or "ERROR" in production.
  • The sdk object is a module-level singleton. Import it from any module and it will reference the same initialized instance.
Open one stream per process, not one per user or request. Scope travels on each call instead. The callbacks each take one argument: on_reconnect receives the attempt count, on_disconnect the reason.
The API key is read fresh every time the SDK starts. Leave SYNAP_API_KEY in your .env (or secrets manager); the same key keeps working until you revoke it in the Dashboard.
4

Create the FastAPI Application with Lifespan

Now create the main.py file. Start with the application lifespan manager, which ensures the SDK initializes on startup and shuts down cleanly when the server stops.
The lifespan context manager is the recommended way to manage startup/shutdown in modern FastAPI applications (v0.95+). It replaces the older @app.on_event("startup") and @app.on_event("shutdown") hooks.
5

Build the Chat Endpoint

Add the chat endpoint to main.py. This endpoint performs five operations in sequence:
  1. Report the incoming user message on the stream
  2. Retrieve relevant memories from Synap
  3. Build a system prompt enriched with memory context
  4. Call the LLM with the enriched prompt
  5. Report the assistant’s reply on the stream
Reporting each turn with send_message is what makes the conversation retrievable: it registers the conversation and appends the turn to its history, exactly as record_message does over REST. Context fetched by conversation_id only returns turns that were reported, so an unregistered conversation returns empty results by design.
Let’s break down each step:
sdk.instance.send_message() publishes the turn on the open stream. Synap appends it to the conversation’s rolling history and registers the conversation under this conversation_id, the same effect conversation.record_message() has over REST. That registration is what later lets conversation.context.fetch(conversation_id=...) resolve scope and return the conversation’s turns. Skip it and the first fetch for a brand-new conversation_id comes back empty, by design.Reporting it also tells Synap what the agent is doing, so it can anticipate what context to push next.
user_id is always required. On a B2B instance customer_id is required as well, and if either is missing the turn is dropped server-side with no error. On a B2C instance customer_id is not accepted: sending it fails with HTTP 400, so pass user_id alone. Never reuse the user identifier as the customer identifier. This example forwards customer_id only when the caller supplied one, so it stays correct on both shapes. GET /api/v1/auth/whoami returns your instance’s user_context_isolation if you are unsure which shape you are on.
The sdk.conversation.context.fetch() call searches Synap’s memory stores for memories relevant to the user’s message. Key parameters:
  • search_query: A list of strings used for semantic search. Passing the user’s message ensures we find contextually relevant memories.
  • max_results=5: Limits context to the top 5 most relevant memories, keeping the prompt concise.
  • types=["facts", "preferences"]: Retrieves only facts and preferences. Other types include episodes, emotions, and temporal. Use all to retrieve every type.
  • mode="fast": Uses the fast retrieval path (lower latency). Use accurate when precision matters more; accurate adds graph traversal, LLM subquery decomposition and reranking on top of the same vector search.
The retrieved memories are formatted as bullet points and injected into the system prompt. This gives the LLM access to user-specific context without modifying the conversation history.The confidence score (e.g., 92%) is included to help the LLM weigh how certain each piece of information is. You can omit confidence scores if you prefer a cleaner prompt.
A standard OpenAI chat completion call. The system prompt now contains personalized context, so the LLM can respond as if it “remembers” the user. This works with any LLM provider: replace the OpenAI call with your preferred provider.
One write, two jobs. send_message(role="assistant", ...) completes the turn in the conversation’s rolling history, so the next turn’s context.fetch sees the full exchange, and it is the event that pre-warms anticipation for the next turn, which is why it belongs after the LLM call, not before.Notice what is not here: memories.create(). Both reported turns become long-term memory on their own, when this conversation compacts: at 3,000 tokens, 10 messages, or 5 minutes of inactivity. Synap promotes the raw turns into the same ingestion pipeline memories.create() would have used.Add explicit ingestion only for content that is not a conversation turn (documents, tickets, backfills) or that must be retrievable sooner than compaction. Never for text you already reported; that extracts it twice. See Agent Integration.
6

Add a Health Check Endpoint

Good practice for production deployments: add a health check that verifies the SDK is connected:
7

Run and Test

Load your environment variables and start the server:
You should see output confirming the SDK has initialized:
Now test with a few conversation turns:
The second turn works because the first was reported with send_message: the exchange is already in the conversation’s history, so context.fetch returns it and the assistant naturally references the Japan trip. Had the turn never been reported, that fetch would come back empty by design.
Long-term memories take a few minutes to appear. Conversation continuity works immediately, as you just saw, but the durable, cross-conversation memories that make memories_used climb are created when the conversation compacts, which for a quiet conversation means about five minutes.To see it without waiting, force compaction once you have sent a few turns:
Give it a moment to process, then fetch again and memories_used will now be non-zero. In production you never call this; the thresholds and the idle timer handle it.
8

Verify in the Dashboard

Open the Synap Dashboard and navigate to your instance. You should see:
  • API call counts reflecting your test requests
  • Memory counts showing extracted facts, preferences, and entities
  • Ingestion history with the conversation turns you sent
Dashboard showing API calls and memory counts

The instance detail page shows API activity and memory counts after your test requests.


Complete Code

Here is the final version of each file for reference. The Python tabs are FastAPI; the JavaScript tabs are Express.

Agent integration

If you are wiring Synap into a live agent (a chat assistant, a copilot, a voice bot, anything with a conversation loop), this is the integration to build. Your agent reports what happened; Synap handles retrieval and memory formation.
Agent integration flow: sdk.initialize() initializes the SDK, then sdk.instance.listen() starts listening once at startup, then a repeating per-turn block of send_message(user_message), fetch() served from cache, your LLM, and send_message(assistant_message), and finally sdk.instance.stop_listening() on shutdown
That is the whole loop. There is no memories.create() in it.

Why there is no ingestion call

Conversation turns you report with send_message() are persisted to conversation history. When the conversation compacts, Synap promotes those raw turns into the long-term ingestion pipeline: the same extraction stages memories.create() runs, producing memories of the same quality. Compaction happens on any of three triggers: The token and message thresholds are configurable per Instance. The idle trigger is what makes this complete: a two-turn exchange that never approaches the other thresholds still compacts a few minutes after the user stops, and its turns still become memory.
Because the idle trigger catches whatever the thresholds miss, every conversation reaches long-term memory eventually. Reporting turns with send_message() is sufficient on its own.

Open one stream for the process

This step is the one JavaScript restriction. Every other call in this loop runs on Edge and Workers; listen() is gRPC, so it needs Node plus @grpc/grpc-js and @grpc/proto-loader. Importing the SDK in an Edge route stays safe, only the stream is unavailable there.
See Real-Time Anticipation in a Server for quotas, reconnects, and the health check worth alerting on.

Report the whole turn, not just the two ends

Most integrations report the user’s message and the assistant’s reply and stop there. That works, and it leaves most of the value on the table. Synap then sees the question and the answer and nothing about the work in between, which is where the next question is usually decided. Five things happen in a turn. Report all five.
If you implement only one of these beyond the user turn, implement assistant_message. Anticipation fires on it. An integration that reports tool calls and reasoning but never the reply gets no prefetching at all, and looks from the outside exactly like one that is working.

Where each call goes

No session call appears in that loop. The SDK opens one on the first event and closes it when you stop listening. end_session is only for a conversation that ends while the process lives on: a call hangs up, a chat window closes.

Check your own work

An integration is finished when all of these are true. If you are an agent wiring this up, check them before you report back.
  • listen() is called once at startup, and stop_listening() on shutdown.
  • Every turn reports a user_message and an assistant_message.
  • Tool calls and results are reported, and share a tool_call_id.
  • Reasoning is reported where the framework exposes it.
  • Every event carries conversation_id and user_id (plus customer_id on B2B).
  • No send_message call sets role by hand for a tool or reasoning event. Use the typed methods; the role and the event type have to agree, and getting that wrong fails silently.
  • end_session is called when a conversation ends and the process keeps running.
Using one of the supported frameworks? Do not write any of this by hand. The integration packages for OpenAI Agents, Google ADK, the Claude Agent SDK and the Vercel AI SDK report these events for you from the framework’s own hooks. See Framework integrations.

Requirements

required
The stream must stay open across turns. This integration suits servers, workers, and voice sessions, not per-request serverless functions, which cannot hold a connection.
required
Stream quotas are per Instance and per client. Opening one per user session exhausts them under real concurrency.
required
Reused across every turn of a conversation. send_message() does not validate it, but fetch() and every other call do.

What still uses REST

The stream is not the entire transport, and it is not meant to be. Two things go over HTTP:
  • initialize(): resolves your client and instance from the API key
  • fetch() on an anticipation miss: pushed bundles cover what Synap predicted; anything it did not predict is fetched normally
You do not code either differently. fetch() is one call whether it resolves from the local anticipation cache in about a millisecond or falls through to the network.

When to still call memories.create()

Promotion is automatic but not instant. A turn becomes retrievable when its conversation compacts, which for a quiet conversation means about five minutes. Reach for explicit ingestion in three cases:
use memories.create
A fact the very next turn depends on, or a live demo showing memory forming. Waiting for compaction is not an option.
use memories.create
Promotion ingests conversation turns as conversation content. To set document_type, mode, custom metadata, or to write at customer or client scope, ingest explicitly.
use memories.create
Product docs, support tickets, CRM records, and backfills belong in ingestion, not on the stream.
Do not do both on the same text. If you report a turn with send_message() and ingest that same turn with memories.create(), the content is extracted twice, once immediately and once at compaction, costing double and producing overlapping memories the deduplication stage then reconciles.Pick one owner per piece of content: stream conversation turns, ingest everything else.

Framework integrations

Five packages drive the stream for you, from the framework’s own hooks. With any other framework, or none, use the loop above directly: it is plain SDK calls and composes with anything. Each package is silent unless listen() is running, so adding one changes nothing until you open a stream.

How anticipation works

The stream in depth: event types, cache behavior, and failure modes.

Running it in a server

Quotas, reconnects, and the silent failure to alert on.

Integration reference

Framework Integration

FastAPI is the most common framework for Synap integrations. Use the lifespan event for initialization and shutdown, and access the SDK instance from your route handlers.
Storing the SDK on app.state (instead of a module-level global) keeps it scoped to the FastAPI app instance, which makes integration tests trivial: TestClient(app) creates a fresh app with its own SDK, and you can replace app.state.sdk with a mock before each test. The Depends(get_sdk) indirection then lets you override the SDK per-test via app.dependency_overrides[get_sdk] = lambda: mock_sdk.

Async/Sync Bridging

Python only. The JavaScript SDK returns promises throughout and there is no synchronous surface to bridge.
The Synap SDK is async-native. If your application uses a synchronous framework, you need to bridge the async calls. Here are the recommended patterns:
Use for scripts, CLI tools, and simple synchronous applications. Creates a new event loop for each call.
asyncio.run() creates a new event loop each time. This is fine for low-frequency calls but adds overhead for high-throughput applications.
Create a single event loop at startup and reuse it for all SDK calls. This is the pattern shown in the Flask and Django examples above.
If you are using Django’s async views alongside sync code, asgiref provides utilities for bridging:

LLM Provider Integration

Full example of a memory-augmented agent using OpenAI’s gpt-4o:

Error Handling and Graceful Degradation

Synap should enhance your application, not be a single point of failure. Design your integration to degrade gracefully when Synap is unavailable.
In JavaScript, branch on error.code, not instanceof. With a dual ESM/CJS dependency graph a consumer can hold two copies of the same error class, and instanceof then fails against the copy it was not built from. isSynapError(e) narrows to Synap’s own taxonomy, so a bug in your code rethrows instead of being swallowed as “Synap was down”. See How it differs from the Python SDK.
Always wrap Synap SDK calls in try blocks in production (try/except in Python, try/catch in JavaScript). Network issues, rate limits, or service disruptions should not prevent your application from responding to users. The LLM can still generate useful responses without memory context; it just won’t be personalized.

What’s Next?

You have a working memory-enabled chatbot. Here are the natural next steps to make it production-ready:

Writing a Use-Case Markdown File

The use-case file is how Synap tunes what gets extracted, how it is stored, and how retrieval ranking works for your Instance.

Multi-User Scoping

Set up memory isolation for multi-tenant applications with user, customer, and client scopes.

Context Compaction

Manage long conversations by compacting context to fit within your LLM’s token budget.

Production Checklist

Security, performance, and monitoring best practices before going live.