Quickstart
From nothing to a retrieved memory in five requests.
GitLoom stores what your agent learns about a user as markdown in a git repository, and gives it back when a later question needs it. This page takes you from nothing to a retrieved memory.
The API is HTTPS and JSON, so every example below is plain curl or the fetch
built into Node 20+. There is also an SDK, if you would rather not write the
requests yourself:
npm install @gitloomhq/sdk
See the TypeScript SDK page once you are past this page — it wraps everything below in a few lines.
0. Set the base URL
export GITLOOM_API="https://api.gitloom.cloud"
Check it without any credential:curl -s "$GITLOOM_API/health"
{"status":"ok"}
/health is the only unauthenticated route. Everything else returns 401 without a
credential.
1. Get an API key
Sign in to the dashboard at https://app.gitloom.cloud and create a key. The key is
shown once, at creation; only its hash is stored, so it cannot be recovered or
re-displayed. If you lose it, revoke it and mint another.
Keys look like this:
gl_live_3f9a2b7c1d4e6f80_kQ8vN2xR7mZpLd3Wc0Ty5Ub9Aj1Hs4Ge
└┬─┘└─┬┘ └──────┬───────┘ └──────────────┬──────────────┘
│ │ │ └─ secret, never stored
│ │ └─ key id: appears in logs, used to revoke
│ └─ environment: live | test
└─ prefix, so secret scanners can match it
Export it:
export GITLOOM_KEY="gl_live_3f9a2b7c1d4e6f80_kQ8vN2xR7mZpLd3Wc0Ty5Ub9Aj1Hs4Ge"
Confirm the key resolves to your account:
curl -s "$GITLOOM_API/v1/whoami" \
-H "Authorization: Bearer $GITLOOM_KEY"
{"account":"acme","auth":"api_key","env":"live"}
## 2. Create a namespaceA namespace is one memory — its own git repository, its own index, isolated from every other namespace in your account. The usual pattern is one namespace per end user.
Namespaces are created explicitly. Writing to one that does not exist is a 404, not
an implicit create — a typo in a user id must not quietly mint an empty memory and
report success.
curl -s -X POST "$GITLOOM_API/v1/namespaces" \
-H "Authorization: Bearer $GITLOOM_KEY" \
-H "content-type: application/json" \
-d '{"namespace":"user-8213"}'
{"namespace":"user-8213","created":true}
Calling it again is success, not a conflict:
{"namespace":"user-8213","created":false}
201 means it was created, 200 means it already existed. Because it is idempotent
you can call it on every application startup without branching.
A namespace name may contain lowercase letters, digits and -, up to 64 characters.
Anything else is 400 invalid_namespace. Omitting namespace gives you default,
which still has to be created before it can be used.
List them:
curl -s "$GITLOOM_API/v1/namespaces" \
-H "Authorization: Bearer $GITLOOM_KEY"
{"namespaces":["default","user-8213"]}
3. Write a memory
You do not write memories. You send a conversation, and GitLoom extracts the memories from it.
curl -s -X POST "$GITLOOM_API/v1/memories" \
-H "Authorization: Bearer $GITLOOM_KEY" \
-H "content-type: application/json" \
-d '{
"namespace": "user-8213",
"session_id": "chat-2026-07-31-a",
"date": "2026-07-31",
"messages": [
{"role": "user", "content": "I finally bought the Sony A7III today — 142k at Fotocentre in Bengaluru. Went with it over the A7IV purely on price."},
{"role": "assistant", "content": "Nice pick. With the 28-70 kit lens you already have, that pairs well for travel."},
{"role": "user", "content": "Yeah, I want it ready before the Japan trip in October."}
]
}'
{"id":"3f2b9e01-7c44-4d1a-9f6e-0a1b2c3d4e5f","namespace":"user-8213","status":"accepted"}
202, not 200. The memory does not exist yet. Extraction is several model
calls, so the write is queued and a worker performs it. See
Limits and behaviour for what that means in practice — in short, expect
seconds, not milliseconds, and there is no status endpoint to poll. The id is the
queue message id, useful in a support conversation and nothing else.
Fields:
| field | required | notes |
|---|---|---|
messages |
yes | at least one turn; role is user or assistant |
namespace |
no | defaults to default, which must still exist |
session_id |
no | your own id for the conversation; carried into ingestion |
date |
no | YYYY-MM-DD or RFC 3339. The date the conversation happened, not now — a backfill of last year's transcripts must not claim everything happened today. Omitted or unparseable falls back to now (UTC). |
The 404 you will hit first:
{"error":{"code":"namespace_not_found","message":"create namespace \"user-8213\" before writing to it"}}
That check runs at the front door, while you are still listening, rather than in the
worker — by the time the worker sees a job you already have a 202 in hand.
4. Retrieve
Give it the question, verbatim.
curl -s -G "$GITLOOM_API/v1/retrieve" \
-H "Authorization: Bearer $GITLOOM_KEY" \
--data-urlencode "q=what camera did I buy" \
--data-urlencode "namespace=user-8213"
{
"tenant": "acme",
"namespace": "user-8213",
"query": "what camera did I buy",
"hits": [
{
"path": "facts/camera-gear/bought-a-sony-a7iii-1f4b9c2a.md",
"score": 0.0328,
"snippet": "Bought a Sony A7III on 2026-07-31 from Fotocentre in Bengaluru for ₹142,000, chosen over the A7IV on price."
},
{
"path": "facts/japan-trip/trip-planned-for-october-7d2e05b1.md",
"score": 0.0061,
"snippet": "Planning a trip to Japan in October 2026; wants the new camera ready before it."
}
],
"millis": 118,
"lexical_ms": 6,
"vector_ms": 103,
"graph_ms": 4,
"cold_start": false
}
Parameters:
| param | default | notes |
|---|---|---|
q |
— | required; 400 if empty |
namespace |
default |
must exist, or 404 |
limit |
24 |
number of hits returned |
Notes on the response:
scoreis a fusion rank, not a similarity. Retrieval runs three arms — lexical BM25, semantic over cue vectors, and a one-hop walk of the relationship graph — concurrently, and fuses them by reciprocal rank (k=60). Values are small: a hit ranked first by one arm scores ≈0.016, first by two arms ≈0.033, and a pure graph neighbour ≈0.006. Compare scores within one response only; there is no absolute threshold that means "relevant".snippetis the opening of the memory, up to 240 characters. It is not a query-focused highlight. Fetch nothing else — for most questions the snippets are the answer material.- The
*_msfields tell you where time went.vector_msdominating is normal and expected: it is the query-embedding round trip to Bedrock, not search. hitsis absent, not[], when nothing matched. Treat a missing key as empty.
5. The same thing in TypeScript
Node 20+, no dependencies. This is the shape the forthcoming TypeScript SDK wraps —
it is not on npm yet, so the runnable version today is fetch.
const API = process.env.GITLOOM_API!;
const KEY = process.env.GITLOOM_KEY!;
async function gl<T>(
path: string,
init: RequestInit & { query?: Record<string, string> } = {},
): Promise<T> {
const url = new URL(path, API);
for (const [k, v] of Object.entries(init.query ?? {})) url.searchParams.set(k, v);
const res = await fetch(url, {
...init,
headers: {
authorization: `Bearer ${KEY}`,
...(init.body ? { "content-type": "application/json" } : {}),
...init.headers,
},
});
const body = await res.json().catch(() => ({}));
if (!res.ok) {
// Two error shapes exist today: /v1/retrieve returns {error: string},
// everything else returns {error: {code, message}}.
const e = (body as any).error;
const code = typeof e === "object" ? e.code : e;
const message = typeof e === "object" ? e.message : e;
throw new Error(`gitloom ${res.status} ${code ?? "error"}: ${message ?? ""}`);
}
return body as T;
}
// 2. create the namespace — idempotent, safe on every startup
await gl<{ namespace: string; created: boolean }>("/v1/namespaces", {
method: "POST",
body: JSON.stringify({ namespace: "user-8213" }),
});
// 3. hand over a conversation; returns 202, the memory does not exist yet
const accepted = await gl<{ id: string; namespace: string; status: string }>(
"/v1/memories",
{
method: "POST",
body: JSON.stringify({
namespace: "user-8213",
session_id: "chat-2026-07-31-a",
date: "2026-07-31",
messages: [
{
role: "user",
content:
"I finally bought the Sony A7III today — 142k at Fotocentre in Bengaluru.",
},
{ role: "assistant", content: "Nice pick, that pairs well with your 28-70." },
],
}),
},
);
console.log(accepted.status); // "accepted"
// 4. retrieve — after ingestion has run; see Limits and behaviour
type Hit = { path: string; score: number; snippet: string };
type Retrieved = {
tenant: string;
namespace: string;
query: string;
hits?: Hit[];
millis: number;
lexical_ms: number;
vector_ms: number;
graph_ms: number;
cold_start: boolean;
};
const r = await gl<Retrieved>("/v1/retrieve", {
query: { q: "what camera did I buy", namespace: "user-8213", limit: "8" },
});
for (const hit of r.hits ?? []) {
console.log(hit.score.toFixed(4), hit.path);
console.log(" ", hit.snippet);
}
Where this goes next
The retrieved snippets are context, not an answer. The intended shape is: retrieve (milliseconds, no model call), then hand the hits to whatever model is already answering your user. GitLoom deliberately does not make that call for you on the read path — that is what keeps retrieval fast and its cost predictable.
- Core concepts — accounts, namespaces, memories, cues, and why git
- Limits and behaviour — asynchrony, latency numbers, and what is not enforced yet
- API reference — every route, and the full OpenAPI description
- Plans and pricing — what Free includes, and what happens past it