How should you design APIs for memory services?

Short answer: Split fast write acceptance from slow commit, make scope mandatory on store and search, and keep REST and SDK semantics equivalent.

Rate limits protect the write path; API design decides whether agents can use it without inventing their own protocol. Weaviate Engram returns a run_id from POST memories, keeps search as a dedicated request with retrieval configuration, and mirrors that model in Python sync and async clients. Agents need low latency on the hot path; extraction and reconciliation do not. This chapter covers which request fields are contract—not decorations—user, group, properties, topics—and how application wrappers stay thin: construct the client once, pass scopes from authenticated identity, size hybrid limits to the context budget, and never swallow auth or validation failures into empty memory lists that teach hallucination. Log run_id with request ids; keep SDK upgrades paired with scoped canary contract tests in CI.

Rate limits protect the write path. API design decides whether agents can use that path without inventing their own memory protocol. A good memory API separates fast accept from slow commit, makes scope mandatory on both store and search, and offers the same semantics through REST and SDKs. Weaviate Engram is built that way: POST memories return a run_id, search is a dedicated request with retrieval configuration, and Python clients mirror the HTTP model for sync and async apps. This chapter walks those patterns and shows how to shape application wrappers so frameworks stay thin.

Why Should Memory APIs Split Write Acceptance From Recall?

Agents need low latency on the hot path. Extraction and reconciliation do not. Engram’s store endpoint accepts conversation, string, or pre-extracted input and returns immediately with a run status. Commit happens in the pipeline. Search stays a synchronous request and response, because the agent needs ranked memories before the next model call.

That split should show up in your own facade even if you only call Engram. Do not expose a single “remember and wait” method as the default. Offer add that returns a run handle, and search that returns content. Reserve waits for tests, migrations, and rare consistency gates. Callers who block every turn on commit recreate the latency tax memory was meant to remove.

REST makes the split obvious with different routes. SDKs should keep the same nouns. client.memories.add and client.memories.search teach the model better than a grab bag of helpers with overlapping names.

How Do REST and SDK Surfaces Stay Equivalent Without Duplicating Complexity?

Engram’s public service speaks HTTPS with bearer API keys. The Python package weaviate-engram wraps the same resources. Use REST from languages without an official client, from edge workers, or from quick operational curls. Use the SDK in application servers where typed retrieval configs and async clients reduce boilerplate.

Keep payloads aligned. Conversation input is a list of role and content messages. String input is raw text for extraction. Pre-extracted input skips extract when you already own structuring. Search bodies carry query, optional user_id, group, properties, topics, and retrieval_config. Hybrid retrieval is the default recommendation for most product queries because it blends semantic and keyword signals.

Version your own wrapper, not Engram’s concepts. If you rename scopes for local taste, map them explicitly to Engram property keys. Hidden translations are how two services drift until search returns another tenant’s slip.

Which Request Fields Are Part of the Contract, Not Optional Decorations?

Scopes are safety. Project identity comes from the API key. User-scoped topics require user_id on add and search. Property scopes such as a boat slip or conversation id must be present when the topic demands them. Omitting a property on search widens the query across values. That is useful for cross-session recall. It is dangerous if you meant hard isolation and forgot the key.

Groups bundle pipeline and topic configuration. Pass the group you configured for the use case instead of silently relying on default once you have more than one. Topics on search further narrow which memory kinds enter context. Fetch retrieval exists for bounded single-object topics where ranking by query is the wrong tool.

Errors should be actionable. Distinguishing auth failure, unknown topic, and validation of missing scope properties lets agents and operators recover differently. Swallowing all failures into an empty memory list teaches the model to hallucinate continuity.

How Should Application Code Call Weaviate Engram in Practice?

Prefer small adapters owned by your service. Construct the client once. Pass scopes from authenticated identity, never from raw client JSON alone. On the read path, search with hybrid limits sized to the context budget. On the write path, send the latest turn or a short window, not the entire transcript every time, unless a summary topic is specifically maintaining that window.

Choose AsyncEngramClient when the host process is already async and many users are in flight. The method names stay the same. Concurrent searches no longer block each other on a single thread. Keep rate gates from the previous chapter in front of add so the elegant API cannot be used to flood itself.

Here is a canoe livery desk that uses the Engram SDK the way a thin REST-equivalent adapter would: scoped add, hybrid search, and no wait on the interactive path.

import os
from engram import EngramClient, HybridRetrieval

client = EngramClient(api_key=os.environ["ENGRAM_API_KEY"])
group = "canoe_livery"
dockhand = "dock-elena"
slip = "canoe-slip-4"


def remember_turn(user_text: str, assistant_text: str) -> str:
    """Fire-and-forget write; mirrors POST /v1/memories semantics."""
    run = client.memories.add(
        [
            {"role": "user", "content": user_text},
            {"role": "assistant", "content": assistant_text},
        ],
        user_id=dockhand,
        group=group,
        properties={"slip_id": slip, "cost_center": "rentals"},
    )
    return run.run_id


def recall_for_prompt(question: str) -> list[str]:
    """Synchronous recall; mirrors POST /v1/memories/search."""
    hits = client.memories.search(
        query=question,
        user_id=dockhand,
        group=group,
        properties={"slip_id": slip},
        retrieval_config=HybridRetrieval(limit=4),
    )
    return [m.content for m in hits]


run_id = remember_turn(
    "Slip 4's red canoe needs the stern line replaced before the evening tide, "
    "and renters under sixteen require a guardian on shore.",
    "Noted. I will keep the stern-line and guardian rules for slip 4.",
)
memories = recall_for_prompt("What safety and rigging rules apply to canoe slip 4?")
print({"run_id": run_id, "memories": memories})

The same flow over REST would POST conversation JSON to /v1/memories and later POST a search body to /v1/memories/search with the identical scope fields. Pick the transport. Keep the contract.

What API Habits Age Well as Agents Multiply?

Document the identity model beside the endpoints. Show which fields are auth-derived and which are product scopes. Provide examples for conversation, string, and pre-extracted inputs so integrators do not invent a fourth shape. Publish retrieval defaults and when to switch to BM25, vector-only, or fetch.

Expose run status for operators and tests without forcing it into every product turn. Log run_id with request ids so pipeline failures can be traced. Keep SDK upgrades paired with contract tests that add and search one scoped canary in CI.

Memory APIs work when they are boring in the right places. Accept writes fast. Search with explicit scopes and retrieval types. Offer REST and SDK parity. Let Engram own extraction while your wrapper owns identity mapping. Authentication then becomes the next hard edge of that contract.

Our next chapter, How do you manage authentication and API keys for memory systems?, covers how to issue, rotate, and scope credentials so those REST and SDK calls stay trusted as fleets grow.