Smart Services

by wizewerx

Docs
PricingConsole

Memory

Semantic recall plus sourced claims, where every statement carries a verbatim citation — for answers that must not invent.

What it is

Memory stores what your app needs to recall later and gives it back two ways: semantic chunks, for cheap lookup over text and uploaded documents, and sourced claims, where every statement carries a verbatim quote and a page number from the document that made it. One service, both halves — enabling Memory enables claims too.

Before you start

You need your app's API key. Get it from the Smart Services console: your app → Settings. Keys are per-app; the key scopes every call to your app's data and nothing else.

The base URL is https://smart-services.io.

Send the key in either header — both are accepted, and Authorization wins if you send both:

Authorization: Bearer <key>
x-api-key: <key>

Memory must be enabled for the app. If it is not, every /api/v1/memory/* and /api/v1/claims/* route answers 403 with Enable Memory in App Settings > Services. Apps provisioned on the older rag or vector service keys are covered too — they do not need re-provisioning.

Your first call

Store a piece of text. It is embedded and searchable by the time the call returns.

curl -X POST "https://smart-services.io/api/v1/memory/ingest" \
  -H "Authorization: Bearer $SMART_SERVICES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Refunds are issued within 14 days of receipt.",
    "collection": "policies",
    "metadata": { "source": "handbook" }
  }'

The same call in JavaScript:

const res = await fetch('https://smart-services.io/api/v1/memory/ingest', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SMART_SERVICES_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    text: 'Refunds are issued within 14 days of receipt.',
    collection: 'policies',
    metadata: { source: 'handbook' },
  }),
});

const stored = await res.json();

Response:

{
  "mode": "text",
  "status": "stored",
  "collection": "policies",
  "model": "bge-m3",
  "count": 1,
  "ids": ["0f8d2c1a-6b34-4c07-9f5e-2a1d8c3b7e40"]
}

ids are what you pass back to /api/v1/memory/forget or GET /api/v1/memory/{id}. To read it back, POST /api/v1/memory/search with { "query": "how long before I get my money back", "collection": "policies" }.

The rest of the endpoints

Memory — chunks and recall:

MethodPathWhat it does
POST/api/v1/memory/ingestStore text (sync), text with your own vector, or queue a sourceUrl/uploadId document for extraction and chunking (async, returns documentId + jobId)
POST/api/v1/memory/searchRecall by query text (the platform embeds it) or by a pre-computed vector. Searches text entries and document chunks together and returns one ranked list
GET/api/v1/memory/{id}?collection=Fetch one stored item by id — text entry or document chunk
POST/api/v1/memory/forgetRemove by ids, documentId, filter, or all: true
GET/api/v1/memory/collectionsList your collections with textEntries, documentChunks and dimensions counts
POST/api/v1/memory/embedRaw embeddings for similarity work you do yourself. OpenAI-shaped response

Claims — sourced statements with citations:

MethodPathWhat it does
POST/api/v1/claims/ingestAccept a document (uploadId or sourceUrl) under a domainKey. Extracts pages. Returns documentId, jobId, status
POST/api/v1/claims/extractTurn a document's pages into claims. Requires the document to be pages_ready
POST/api/v1/claims/queryAsk a domainKey a question. Returns claims with provenance, never prose
GET/api/v1/claims/documents?domainKey=&documentId=&limit=Per-document status, claimCount, error, warnings[], conventions[] and a pages health breakdown

Async document ingestion goes through the jobs pipeline. Poll /api/v1/rag/jobs/{jobId} for a memory document, or pass webhookUrl and be told.

Limits and errors

Errors come back as JSON with an error string, sometimes with a details string that says what to do instead. Examples the routes actually return:

{
  "error": "Memory service is not enabled for this app",
  "details": "Enable Memory in App Settings > Services."
}
{
  "error": "Nothing to forget",
  "details": "Provide \"ids\", \"documentId\", a non-empty \"filter\", or \"all\": true. An empty target is refused rather than treated as \"everything\"."
}

Status codes: 400 malformed body or a request we refuse to guess at, 403 Memory not enabled, 404 no such document, upload or item, 500 storage or embedding failure, 502 from /memory/embed when the embedding backend is unreachable.

Memory is a tiered service billed on stored bytes, with a monthly reset. Claims are inside that tier — ingest, extraction and embedding are all covered by it, and there is no second thing to buy. Your app's current tier, quota and usage are shown in the console under App Settings → Services; that is the live number, so read it there rather than from any figure written down elsewhere.

topK on search is clamped to 1–100. limit on /api/v1/claims/documents is clamped to 1–200.

Gotchas

A claim is what the source asserts, not a fact about the world. If a client's handbook is wrong, the pool faithfully records that their handbook says it. We warrant that we read it correctly, not that they wrote it correctly. Two claims disagreeing is normal.

no_claim_made is an answer, not an empty result. Query returns one of three statuses: answered, no_claim_made (the domain has claims, none about this) and empty_domain (the domain has no claims at all). no_claim_made is the binding, bounded answer that chunk retrieval structurally cannot give you, because an empty top-k is indistinguishable from a bad embedding. We can report it; we cannot stop your model inventing anyway. Do not sell your users a guarantee you only half control.

Surface metadata.extractionWarnings to whoever uploaded the file. A document with unreadable pages — usually scans — produces a pool that fails authoritatively: it says "no claim made" about content plainly visible in the PDF. That is worse than a chunk search coming back empty. A document we cannot read at all fails loudly with status: "failed"; it never silently succeeds with nothing in it.

Check document.pages, not just the job. Claim extraction can fail partially, and partial success is deliberately still a job success — failing it would discard work already done. So job.status cannot distinguish a clean run from one where a third of the document was unreadable. GET /api/v1/claims/documents gives you pages broken down by extracted / empty / failed / pending, plus document.error.

documentDate is when the source was written, not when you uploaded it. It is usually absent from the text and unknowable from it, so you declare it. It is reserved for recency-based supersession — which is not wired up yet. Supersession edges exist and are returned in supersedes, but nothing creates them automatically. The naive choice, our extraction timestamp, is actively wrong: re-extract an old document today and a 2019 policy would supersede the 2026 one.

domainKey is an opaque namespace, never parsed as a hostname even when it looks like one. It is lowercased and trimmed, so XYZ.com and xyz.com are the same namespace. There is no provisioning step — the first write creates it.

Ingest and extract are separate on purpose. Pages are re-readable, so a better extractor re-reads stored pages without re-fetching, re-parsing or re-charging. /claims/extract returns 409 if the document is not yet pages_ready, and it runs with one attempt rather than a retry policy, because a retry re-runs only pages still pending or failed. Resumption is a fresh call.

Claim selection is hybrid, and a semantic hit must clear a similarity floor of 0.45. Embeddings over claim statements run alongside Postgres full-text and the results interleave, best-of-each first — they are not score-merged, because a cosine similarity and a ts_rank are different units. The floor is what keeps no_claim_made reachable: a vector index always returns its nearest neighbours however distant. Measured separation on real claims: relevant paraphrase 0.55–0.72, unrelated subjects 0.20–0.33. Omit question to enumerate the whole domain, bounded by limit.

citationCount is free signal. A claim is the union of its restatements, so 1,000 posts restating 20 claims is 20 claims with many provenance entries each. 47 citations is a core message; 1 from 2019 is an orphan.

Supplying your own vector to ingest records model: null. That is deliberate — we cannot claim your vectors came from the platform model, and null is the signal that the collection may hold mixed generations. You must supply one vector per text, all the same dimensionality, or the call is refused rather than guessed at.

forget refuses an empty target. An empty filter would otherwise mean "match everything", which is an easy way to destroy a corpus by accident. all: true has to be explicit. forget with all drops both backing namespaces for that collection — text entries and document chunks.

Use /memory/search, not /vector/search. The legacy vector route makes you embed first and pass number[], which means every consumer wires up its own embedding call and can silently drift onto a different model. /memory/search takes text and embeds with the platform contract, so results stay comparable. The same retrieval code backs chat-completions memory augmentation, so the two can never return different views of a collection.

A model name never enters a claims call. Extraction model, provider and endpoint are platform implementation detail, covered by the tier. That is not just pricing: provenance quality is model-dependent, and a model that paraphrases quotes would silently degrade the verbatim citations the product guarantees. A claim whose quote cannot be located in the source is discarded, never stored.