It is the OpenAI API, with a memory behind it.
There is no SDK to learn. Change the base URL and the key in whatever you already use, add a user so Varve knows whose memory to draw on, and everything else works as it did.
Quick start
Base URL https://api.varve.it
· authenticate with
Authorization: Bearer vrv_live_…
curl https://api.varve.it/v1/chat/completions \
-H "Authorization: Bearer $VARVE_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek/deepseek-v4-flash",
"user": "alice@yourcompany.com",
"messages": [{"role": "user", "content": "Where am I based again?"}]
}'Machine-readable spec:
/v1/openapi.json — OpenAPI 3.1, which
generates a client in most languages without anyone writing one.
Authentication
Every request carries a bearer key. Keys can be scoped to
any subset of read, write, chat and
erase, so a key embedded in a client to read memory need not
also be able to write to it or spend your budget.
user is the one body field that matters. It
is the person the memory belongs to, and a request without it is refused
rather than guessed at — two callers who both leave it out would
otherwise share one memory. Send the same stable identifier you use
everywhere else: an email, a Slack id, your own user id.
| Authorization | Bearer vrv_live_…, or
vrv_test_… against a sandbox twin
that cannot spend money. |
| user | Required on /v1/chat/completions. Whose memory this
is. |
| X-Memory-Subject | The same thing as a header, for clients that will not let you add a body field. |
Talk to your model
| POST | /v1/chat/completions | OpenAI-compatible: point any SDK at this URL — one base_url change. Varve retrieves what it knows about user, puts it in front of your messages in a cache-stable order, calls your provider on your key, and records the turn. Three differences from a raw provider: stream:true is answered as one complete response; incoming system messages are replaced by Varve's own instruction rather than passed through; and a message sent as content parts is forwarded to your model whole — images included — while Varve reads and remembers only its text. Everything else in the body, tools and response_format among them, is passed upstream untouched. |
See what is remembered
| GET | /v1/memory/timeline | Current beliefs and the supersession chain: what is held now, what it replaced, and when. ?subject= scopes it to one person. ?as_of=2026-03-01 answers it as of a date instead — the facts that had started and had not yet been retired at that moment, returned as one list rather than split into current and superseded, because on that date they were simply true. |
| GET | /v1/memory/explain?q= | The playground, as data. For a question, exactly which facts would be injected, which were withheld as superseded, the retrieval path taken and what each stage cost in milliseconds. |
| GET | /v1/memory/facts | A flat list of current facts. |
| GET | /v1/memory/audit | Every change, newest first, with the recorded actor — automated changes all say worker. Keyset paged: pass before= the last id you saw. |
Fix what is wrong
| POST | /v1/memory/fact/{id}/correct | {"actor":"you","object":"the right value","reason":"why"}. Supersedes the fact and marks it human-set, which stops extraction reversing it later. |
| POST | /v1/memory/fact/{id}/retire | {"actor":"you"}. The fact stops being current with nothing replacing it, and stays visible as history. |
| GET | /v1/memory/conflicts | Places where extraction wanted to change something a person set. Nothing is auto-resolved. |
| POST | /v1/memory/conflict/{id}/resolve | {"actor":"you","choice":"held"|"proposed"}. |
Answer a data request
| GET | /v1/subjects | Everyone the system holds data about. |
| GET | /v1/subject/{id}/export | Everything held about one person. |
| DELETE | /v1/subject/{id}?confirm={id} | Crypto-shredded erasure: destroys that person's key, so the record becomes unreadable in the live system and in every backup taken afterwards; older backups age out on a 14-day cycle. |
Send us a conversation from anywhere
| POST | /v1/ingest/{source_id} | One URL for any source you like — your own app, a CRM, a helpdesk, a transcriber. Body: {"external_id":"who","text":"what they said"}, plus optional at, session, thread and id. Authenticated by signature, not by API key, so your server posts without holding a token that can also read and spend. |
Operations
| GET | /v1/usage | Calls, tokens, cache hit rate, cost and the counterfactual. |
| GET | /v1/status | Fact counts, queue depth, connected sources. |
| GET | /healthz | No key required. |
Signing an ingest request
Ask us for a source and you get a URL and a secret. Sign the timestamp, a dot, and the raw body — the same scheme Stripe and Slack use on their own webhooks.
ts = str(int(time.time()))
body = json.dumps({"external_id": "ana", "text": "We ship on Thursdays."})
sig = hmac.new(SECRET.encode(), f"{ts}.{body}".encode(),
hashlib.sha256).hexdigest()
requests.post(f"https://api.varve.it/v1/ingest/{SOURCE_ID}",
data=body,
headers={"Content-Type": "application/json",
"X-Varve-Timestamp": ts,
"X-Varve-Signature": "v1=" + sig})A signature is good for five minutes. Retrying is safe: a turn is keyed on the id you give it, so sending the same message twice stores it once and tells you it was a duplicate — you never have to choose between at-least-once delivery and duplicate memories.
Before you build on it
Your key, your bill
Varve calls your provider with the key you gave it. We never proxy your billing and never mark it up.
Order is not cosmetic
The prompt is assembled so the stable part comes first and stays byte-identical between turns. That is what makes the provider's cache hit.
Nothing is overwritten
A changed fact supersedes the old one, which stays readable with a pointer to its replacement. History is a feature, not a leak.
Errors say whose fault it is
A 429 carries
X-RateLimit-Source, so you can tell our limit from your
provider's rather than guessing.