{
  "info": {
    "name": "ai.2brother.in — RAG API",
    "description": "Multi-tenant retrieval-augmented generation over pgvector + Neo4j, with Deepgram transcription and usage metering. OpenAI-compatible chat and embeddings.\n\n## Setup\n1. Select the environment or edit the collection variables.\n2. Set `admin_key` (from the server's ADMIN_BOOTSTRAP_KEY) to use the Admin folder.\n3. Run **Admin > Seed plans**, then **Admin > Create tenant** — it captures `api_key` automatically.\n4. Everything else uses `{{api_key}}`.\n\nAuth is `Authorization: Bearer <key>` on every tenant endpoint.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "variable": [
    {
      "key": "base_url",
      "value": "https://ai.2brother.in",
      "type": "string"
    },
    {
      "key": "api_key",
      "value": "",
      "type": "string"
    },
    {
      "key": "admin_key",
      "value": "",
      "type": "string"
    },
    {
      "key": "document_id",
      "value": "",
      "type": "string"
    },
    {
      "key": "tenant_id",
      "value": "",
      "type": "string"
    },
    {
      "key": "conversation_id",
      "value": "demo-thread-1",
      "type": "string"
    }
  ],
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{api_key}}",
        "type": "string"
      }
    ]
  },
  "item": [
    {
      "name": "Health",
      "item": [
        {
          "name": "Liveness",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/health",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "health"
              ]
            },
            "description": "Cheap liveness probe. Returns 200 whenever the process is up."
          }
        },
        {
          "name": "Readiness (deep)",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "const j = pm.response.json();",
                  "pm.test('postgres reachable', () => pm.expect(j.postgres).to.be.true);",
                  "pm.test('redis reachable',    () => pm.expect(j.redis).to.be.true);",
                  "if (!j.neo4j) console.warn('Neo4j down — graph retrieval disabled');",
                  "if (!j.inference_node?.primary) console.warn('Mac mini node unreachable');"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/health/ready",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "health",
                "ready"
              ]
            },
            "description": "Checks Postgres, Redis, Neo4j and the Mac mini inference node.\n\nPostgres and Redis are hard requirements (503 if down). Neo4j and the node are reported but do not fail readiness — retrieval degrades to vector+lexical without the graph."
          }
        }
      ]
    },
    {
      "name": "Admin",
      "description": "Platform-operator endpoints. Authenticated with X-Admin-Key, never a tenant key.",
      "item": [
        {
          "name": "Seed plans",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "POST",
            "header": [
              {
                "key": "X-Admin-Key",
                "value": "{{admin_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/admin/plans/seed",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "admin",
                "plans",
                "seed"
              ]
            },
            "description": "Creates the free / starter / growth plans. Idempotent — safe on every deploy."
          }
        },
        {
          "name": "List plans",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "GET",
            "header": [
              {
                "key": "X-Admin-Key",
                "value": "{{admin_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/admin/plans",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "admin",
                "plans"
              ]
            }
          }
        },
        {
          "name": "Create tenant (captures api_key)",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.response.code === 201) {",
                  "  const j = pm.response.json();",
                  "  pm.collectionVariables.set('api_key', j.api_key);",
                  "  pm.collectionVariables.set('tenant_id', j.tenant.id);",
                  "  console.log('Captured api_key for tenant', j.tenant.slug);",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "POST",
            "header": [
              {
                "key": "X-Admin-Key",
                "value": "{{admin_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"slug\": \"acme\",\n  \"name\": \"Acme Corporation\",\n  \"plan_code\": \"starter\",\n  \"owner_email\": \"ops@acme.example\"\n}"
            },
            "url": {
              "raw": "{{base_url}}/v1/admin/tenants",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "admin",
                "tenants"
              ]
            },
            "description": "Creates a tenant and returns its first API key.\n\n**The raw key is shown exactly once.** Only an Argon2 hash is stored, so it cannot be retrieved later — issue a new key instead."
          }
        },
        {
          "name": "List tenants",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "GET",
            "header": [
              {
                "key": "X-Admin-Key",
                "value": "{{admin_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/admin/tenants",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "admin",
                "tenants"
              ]
            },
            "description": "Every tenant with its plan and current-period spend."
          }
        },
        {
          "name": "Issue an additional key",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "POST",
            "header": [
              {
                "key": "X-Admin-Key",
                "value": "{{admin_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"ci-pipeline\",\n  \"scopes\": [\"chat\", \"search\"],\n  \"expires_in_days\": 90\n}"
            },
            "url": {
              "raw": "{{base_url}}/v1/admin/tenants/{{tenant_id}}/keys",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "admin",
                "tenants",
                "{{tenant_id}}",
                "keys"
              ]
            },
            "description": "`scopes` restricts the key. An empty list means full access.\nValid scopes: chat, embeddings, search, ingest, graph."
          }
        },
        {
          "name": "List keys",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "GET",
            "header": [
              {
                "key": "X-Admin-Key",
                "value": "{{admin_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/admin/tenants/{{tenant_id}}/keys",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "admin",
                "tenants",
                "{{tenant_id}}",
                "keys"
              ]
            }
          }
        },
        {
          "name": "Revoke a key",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "DELETE",
            "header": [
              {
                "key": "X-Admin-Key",
                "value": "{{admin_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/admin/keys/:key_id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "admin",
                "keys",
                ":key_id"
              ],
              "variable": [
                {
                  "key": "key_id",
                  "value": ""
                }
              ]
            },
            "description": "Takes effect immediately — the next request with that key gets 401."
          }
        },
        {
          "name": "Suspend / change plan",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "PATCH",
            "header": [
              {
                "key": "X-Admin-Key",
                "value": "{{admin_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/admin/tenants/{{tenant_id}}?status=suspended&plan_code=growth",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "admin",
                "tenants",
                "{{tenant_id}}"
              ],
              "query": [
                {
                  "key": "status",
                  "value": "suspended",
                  "description": "active | suspended | deleted"
                },
                {
                  "key": "plan_code",
                  "value": "growth"
                }
              ]
            },
            "description": "A suspended tenant gets 402 on every billable endpoint."
          }
        },
        {
          "name": "Run billing",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "POST",
            "header": [
              {
                "key": "X-Admin-Key",
                "value": "{{admin_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/admin/billing/run?finalise=false",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "admin",
                "billing",
                "run"
              ],
              "query": [
                {
                  "key": "finalise",
                  "value": "false",
                  "description": "true pushes invoices to the payment provider"
                }
              ]
            },
            "description": "Builds invoices for all active tenants. Idempotent per period. The worker also runs this on the 1st at 02:00 UTC."
          }
        }
      ]
    },
    {
      "name": "Chat",
      "item": [
        {
          "name": "Grounded completion",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('200 OK', () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test('has an answer', () => pm.expect(j.choices[0].message.content).to.be.a('string').and.not.empty);",
                  "if (j.citations) console.log('citations:', j.citations.length);"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"messages\": [\n    {\"role\": \"user\", \"content\": \"Who founded Bluefin Data, and who does the combined team report to?\"}\n  ],\n  \"temperature\": 0.2,\n  \"rag\": {\n    \"enabled\": true,\n    \"use_graph\": true,\n    \"top_k\": 8,\n    \"return_citations\": true\n  }\n}"
            },
            "url": {
              "raw": "{{base_url}}/v1/chat/completions",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "chat",
                "completions"
              ]
            },
            "description": "OpenAI `chat.completion` shape plus two extra fields:\n\n- `citations` — one entry per retrieved chunk, with document, label, score and which retrievers found it. Audio chunks carry `speaker`, `start_s`, `end_s`.\n- `rag` — the rewritten queries, entities used for graph lookup, and counts.\n\nThe `rag` request object is a vendor extension; standard OpenAI SDKs simply omit it and get default behaviour."
          }
        },
        {
          "name": "Off-corpus question (auto fallback)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"messages\": [\n    {\n      \"role\": \"user\",\n      \"content\": \"What is the capital of Japan?\"\n    }\n  ]\n}"
            },
            "url": {
              "raw": "{{base_url}}/v1/chat/completions",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "chat",
                "completions"
              ]
            },
            "description": "No `rag` object at all. `mode` defaults to `auto`, so the service decides for itself whether the corpus answers the question.\n\nHere it does not, so the answer comes from general knowledge, is prefixed with \"Not found in your documents\", and carries **no citations**.\n\nRead `rag.grounded` and `rag.grounding_reason` to see which path was taken."
          }
        },
        {
          "name": "Strict mode (refuse if not in corpus)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"messages\": [\n    {\n      \"role\": \"user\",\n      \"content\": \"What is the capital of Japan?\"\n    }\n  ],\n  \"rag\": {\n    \"mode\": \"strict\"\n  }\n}"
            },
            "url": {
              "raw": "{{base_url}}/v1/chat/completions",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "chat",
                "completions"
              ]
            },
            "description": "`strict` never falls back to general knowledge. Use it where a plausible-sounding general answer would be worse than no answer at all."
          }
        },
        {
          "name": "Streaming (SSE)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Accept",
                "value": "text/event-stream"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"stream\": true,\n  \"messages\": [\n    {\"role\": \"user\", \"content\": \"Summarise the acquisition in one sentence.\"}\n  ]\n}"
            },
            "url": {
              "raw": "{{base_url}}/v1/chat/completions",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "chat",
                "completions"
              ]
            },
            "description": "Server-sent events in OpenAI chunk format, terminated by `data: [DONE]`.\n\nThe **first** frame carries the `citations` array, so a UI can render sources before the first token arrives."
          }
        },
        {
          "name": "Scoped to a collection",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"messages\": [{\"role\": \"user\", \"content\": \"What objections came up on the calls?\"}],\n  \"rag\": {\n    \"filters\": {\"collection\": \"calls\"},\n    \"top_k\": 12\n  }\n}"
            },
            "url": {
              "raw": "{{base_url}}/v1/chat/completions",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "chat",
                "completions"
              ]
            },
            "description": "`filters` is matched with jsonb containment against `documents.meta`, so any labels you attached at ingest time can scope retrieval."
          }
        },
        {
          "name": "Multi-turn (server-side history)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"messages\": [\n    {\"role\": \"user\", \"content\": \"Tell me about the Acme renewal.\"},\n    {\"role\": \"assistant\", \"content\": \"The Acme renewal is due in March.\"},\n    {\"role\": \"user\", \"content\": \"Who owns it?\"}\n  ],\n  \"rag\": {\"conversation_id\": \"{{conversation_id}}\"}\n}"
            },
            "url": {
              "raw": "{{base_url}}/v1/chat/completions",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "chat",
                "completions"
              ]
            },
            "description": "With history present, the elliptical follow-up (\"Who owns it?\") is rewritten into a standalone query before retrieval. Passing `conversation_id` also persists the turns server-side."
          }
        },
        {
          "name": "RAG disabled (passthrough)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"messages\": [{\"role\": \"user\", \"content\": \"Rewrite this politely: send it now\"}],\n  \"rag\": {\"enabled\": false}\n}"
            },
            "url": {
              "raw": "{{base_url}}/v1/chat/completions",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "chat",
                "completions"
              ]
            },
            "description": "Straight to the local model with no retrieval. Still metered."
          }
        },
        {
          "name": "List models",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/v1/models",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "models"
              ]
            }
          }
        }
      ]
    },
    {
      "name": "Retrieval",
      "item": [
        {
          "name": "Hybrid search",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"query\": \"who leads engineering after the acquisition\",\n  \"top_k\": 8,\n  \"use_graph\": true\n}"
            },
            "url": {
              "raw": "{{base_url}}/v1/search",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "search"
              ]
            },
            "description": "Retrieval with no generation. Each result reports `retrieved_by` — `vector`, `keyword`, `graph`, or several — which is the fastest way to diagnose why something did or didn't surface.\n\nAlso returns the graph facts that were in play."
          }
        },
        {
          "name": "Search within specific documents",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"query\": \"pricing terms\",\n  \"document_ids\": [\"{{document_id}}\"],\n  \"top_k\": 5,\n  \"use_graph\": false\n}"
            },
            "url": {
              "raw": "{{base_url}}/v1/search",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "search"
              ]
            }
          }
        },
        {
          "name": "Embeddings",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"input\": [\"first passage to embed\", \"second passage\"]\n}"
            },
            "url": {
              "raw": "{{base_url}}/v1/embeddings",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "embeddings"
              ]
            },
            "description": "OpenAI-shaped. Served by the local embedding model on the Mac mini, so vectors are comparable with everything already indexed."
          }
        }
      ]
    },
    {
      "name": "Ingestion",
      "item": [
        {
          "name": "Ingest text",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.response.code === 202) {",
                  "  pm.collectionVariables.set('document_id', pm.response.json().id);",
                  "  console.log('document_id captured');",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"title\": \"Q3 board notes\",\n  \"content\": \"Priya Sharma is the CTO of Northwind Analytics. In September 2025 Northwind Analytics acquired Bluefin Data, a Bengaluru company that builds streaming pipelines. Bluefin Data was founded by Rahul Menon. The combined engineering team will report to Priya Sharma.\",\n  \"meta\": {\"collection\": \"board\", \"year\": 2025},\n  \"build_graph\": true\n}"
            },
            "url": {
              "raw": "{{base_url}}/v1/ingest/text",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "ingest",
                "text"
              ]
            },
            "description": "Returns **202** immediately with `status: pending`; indexing runs in the worker.\n\n`meta` is free-form and becomes the filter surface for retrieval later.\n\nRe-posting identical content returns the existing document (deduped by SHA-256 per tenant)."
          }
        },
        {
          "name": "Ingest file (document or audio)",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.response.code === 202) pm.collectionVariables.set('document_id', pm.response.json().id);"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "method": "POST",
            "body": {
              "mode": "formdata",
              "formdata": [
                {
                  "key": "file",
                  "type": "file",
                  "src": [],
                  "description": "pdf, docx, txt, md — or mp3/m4a/wav/mp4 for audio"
                },
                {
                  "key": "title",
                  "value": "Client call — 12 Feb",
                  "type": "text"
                },
                {
                  "key": "meta",
                  "value": "{\"collection\":\"calls\",\"deal\":\"acme\"}",
                  "type": "text"
                },
                {
                  "key": "build_graph",
                  "value": "true",
                  "type": "text"
                }
              ]
            },
            "url": {
              "raw": "{{base_url}}/v1/ingest/file",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "ingest",
                "file"
              ]
            },
            "description": "Audio and video are detected by MIME type or extension and routed to Deepgram for diarized transcription. Every resulting chunk keeps its speaker and time window, so answers cite the moment in the recording.\n\nMax 200 MB."
          }
        },
        {
          "name": "Ingest from URL",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"https://cdn.example.com/calls/acme-2025-02-12.mp3\",\n  \"title\": \"Acme call\",\n  \"source_type\": \"auto\",\n  \"meta\": {\"collection\": \"calls\"}\n}"
            },
            "url": {
              "raw": "{{base_url}}/v1/ingest/url",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "ingest",
                "url"
              ]
            },
            "description": "For audio URLs, Deepgram fetches the media directly — the bytes never transit this server."
          }
        },
        {
          "name": "Poll document status",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "const s = pm.response.json().status;",
                  "console.log('status:', s);",
                  "pm.test('not failed', () => pm.expect(s).to.not.equal('failed'));"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/v1/documents/{{document_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "documents",
                "{{document_id}}"
              ]
            },
            "description": "`status` moves pending → processing → ready. On `failed`, the `error` field says why."
          }
        },
        {
          "name": "List documents",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/v1/documents?limit=50&offset=0",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "documents"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "50"
                },
                {
                  "key": "offset",
                  "value": "0"
                },
                {
                  "key": "status",
                  "value": "ready",
                  "disabled": true
                }
              ]
            }
          }
        },
        {
          "name": "Reprocess a document",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/v1/documents/{{document_id}}/reprocess?build_graph=true",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "documents",
                "{{document_id}}",
                "reprocess"
              ],
              "query": [
                {
                  "key": "build_graph",
                  "value": "true"
                }
              ]
            },
            "description": "Re-chunks and re-indexes. Needed after changing the embedding model or chunk size. Replaces chunks wholesale, so it cannot double-index."
          }
        },
        {
          "name": "Delete a document",
          "request": {
            "method": "DELETE",
            "url": {
              "raw": "{{base_url}}/v1/documents/{{document_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "documents",
                "{{document_id}}"
              ]
            },
            "description": "Removes chunks, vectors, the stored upload, and the document's nodes from the graph — including entities left with no remaining mentions."
          }
        }
      ]
    },
    {
      "name": "Knowledge graph",
      "item": [
        {
          "name": "Graph stats",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/v1/graph/stats",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "graph",
                "stats"
              ]
            },
            "description": "Entity, relation and chunk counts for your tenant only."
          }
        },
        {
          "name": "Top entities",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/v1/graph/entities?limit=50",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "graph",
                "entities"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "50"
                }
              ]
            },
            "description": "Ordered by mention count. The same real-world entity across many documents is one node — that merge is what makes multi-hop traversal useful."
          }
        },
        {
          "name": "Entity neighbourhood",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/v1/graph/neighbourhood?name=Northwind Analytics&hops=2&limit=50",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "graph",
                "neighbourhood"
              ],
              "query": [
                {
                  "key": "name",
                  "value": "Northwind Analytics"
                },
                {
                  "key": "hops",
                  "value": "2"
                },
                {
                  "key": "limit",
                  "value": "50"
                }
              ]
            },
            "description": "Returns `{nodes, edges}` ready to hand to a graph renderer."
          }
        }
      ]
    },
    {
      "name": "Billing",
      "item": [
        {
          "name": "Current usage",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/v1/usage",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "usage"
              ]
            },
            "description": "Spend for the calendar month in progress, broken down by kind: chat, embedding, transcription, extraction. Also shows the plan's included amount and hard cap."
          }
        },
        {
          "name": "Usage history",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/v1/usage/history?months=6",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "usage",
                "history"
              ],
              "query": [
                {
                  "key": "months",
                  "value": "6"
                }
              ]
            }
          }
        },
        {
          "name": "Preview invoice",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/v1/invoices/preview",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "invoices",
                "preview"
              ]
            },
            "description": "Draft invoice for the period in progress, recomputed from the usage ledger on each call."
          }
        },
        {
          "name": "List invoices",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/v1/invoices",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "invoices"
              ]
            }
          }
        }
      ]
    },
    {
      "name": "Error cases",
      "description": "Worth running once so you know what failures look like.",
      "item": [
        {
          "name": "401 — bad key",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('401', () => pm.response.to.have.status(401));"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "rag_live_definitely-not-valid"
                }
              ]
            },
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/v1/usage",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "usage"
              ]
            },
            "description": "Errors use the OpenAI envelope: `{\"error\": {\"message\", \"type\", \"code\"}}`, so OpenAI SDKs surface them natively."
          }
        },
        {
          "name": "429 — rate limited",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"query\": \"rate limit probe\"\n}"
            },
            "url": {
              "raw": "{{base_url}}/v1/search",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "search"
              ]
            },
            "description": "Run this in the Collection Runner with a high iteration count to trip the per-tenant limit. The 429 carries a `Retry-After` header."
          }
        },
        {
          "name": "402 — quota exceeded",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/v1/usage",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "usage"
              ]
            },
            "description": "Returned once the tenant passes its plan's monthly hard cap, or when the tenant is suspended."
          }
        }
      ]
    }
  ]
}