GitLoom

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 namespace

A 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:

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.