API

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_…

Fig. 1 a first call, start to finish
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/completionsOpenAI-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/timelineCurrent 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/factsA flat list of current facts.
GET/v1/memory/auditEvery 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/conflictsPlaces 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/subjectsEveryone the system holds data about.
GET/v1/subject/{id}/exportEverything 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/usageCalls, tokens, cache hit rate, cost and the counterfactual.
GET/v1/statusFact counts, queue depth, connected sources.
GET/healthzNo 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.

Fig. 2 signing an ingest request
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.