Skip to content
DocsBuild with itClient integrations

Client Spaces

A useful staff reference, built from your real business.

Start a small restaurant, gym, or general client pilot with approved operating knowledge. Each client gets an explicitly chosen Space for capture and recall.

Run a focused pilot

  1. Create one private Space for an authorized client. Choose a template and edit its purpose. Templates add no facts, documents, or sample memories. Create a Space.
  2. Capture a few approved sources. For a restaurant, use its current menu information, service handbook, and opening checklist. For a gym, use membership policies, class operations, and front-desk FAQs. Keep the gym pilot about staff operations; do not upload health or medical records.
  3. Check the capture’s job. Open Processing and match the returned job ID. This is your account’s queue, not a client-filtered view. A completed job may yield one, several, or no memories; review what was saved.
  4. Test with known questions. Use “Ask this Space” from its page. Check the answer and sources against your approved material, and test a question whose answer is absent. For separate clients, use distinct test documents and confirm each stays out of the other client’s results.
  5. Connect an actual system you control. Start with one reviewed export or a small server integration. Confirm permission, Space mapping, processing, and retrieval before automating additional material.

These templates do not provision POS, booking, or medical-record connectors. An integration with those systems would need its own authorized implementation. Creating a Space sends no invitations or emails. Public signup remains disabled; use an existing authorized account for the pilot.

Create a key restricted to one client Space

  1. Sign in and open Settings → API keys. Explicitly choose Client Space key, then choose the client’s owned, active private Space. Spaces owned by someone else and archived Spaces are unavailable.
  2. Give the key a name of up to 60 characters. Keep Read only for retrieval; deliberately choose Capture + read if the integration must save text. New Client Space keys expire after 90 days by default; you can choose 1–3650 days.
  3. Copy the pm_space_ secret when the form returns it and store it on that client’s backend. It is shown once, never put in a URL, and cannot be retrieved later. The key list shows its Space restriction, permissions, expiry and active or expired status. Revoke an old key after replacing it in your integration.

Privacy follows the existing Space audience rules. A normal user Space is private when its audience is omitted; the same default applies to system, temporary and project Spaces. Organizational Spaces without a private audience and unknown kinds without an audience are unavailable. Sharing your own Space with staff does not by itself change its knowledge-filing audience; a private Space you own remains eligible.

The API binds requests to the key’s Space when selectors are omitted. If you send selectors, use spaceIds: [spaceId] for that exact Space and scope: "space". A different Space, multiple Spaces, an empty selection or universal scope is rejected. The API checks that the Space remains owned, active and private.

Client Space key REST allowlist
RoutePermission and restriction
GET /api/v1/client-spaceread · Verify this key’s Space, scopes and capabilities.
GET /api/v1/searchread · Search the bound Space.
POST /api/v1/contextread · Assemble context from the bound Space.
POST /api/v1/rememberwrite · Text capture only, into the bound Space.
GET /api/v1/jobs/:idread · A job belonging to this exact Space only.
GET /api/v1/documents/:idread · A document belonging to this exact Space only.

Before accepting integration traffic, call GET /api/v1/client-space with the key in the Authorization header. It requires read permission and returns { space: { id, name }, scopes, capabilities } for the bound Space only. Capabilities include search, context and capture:status; capture:text appears only with write permission. Compare the returned ID with your server’s configured mapping and stop on a mismatch or failed verification. Use server-side fetch for this endpoint; it does not require an SDK resource or account Space listing.

All other routes are denied, including Space listing and management, account tools, agents, conversations and chat, MCP, the CLI API, and file uploads. A read-only key cannot capture. URL ingestion, attachments and binary uploads are outside this text-only integration. SDK methods work only when they call an allowed REST route.

Connect through your server

Start with a runnable integration

The Node.js starter maps each client to its own Client Space API key and a separate client token. It includes text capture, job status, capture results, and search endpoints, setup instructions, and tests. Configure it on your server, then connect the client’s approved source or export. This starter needs no additional database.

Download integration starter

Requires Node.js 22+, a Capture + read Client Space key (pm_space_) and one private Space per client. Before starting, it verifies each configured mapping, scopes and capabilities through GET /api/v1/client-space. Missing or mismatched credentials stop startup; there is no account-key fallback. Your developer must connect the existing business software and staff authentication.

For each starter client, configure id, spaceId, apiKeyEnv and tokenEnv. The last two fields name environment variables; put the actual API key and separate client token in your server secret store, not the JSON file. Use distinct credentials for each client. The client token authenticates requests to the starter; the Client Space key authenticates the starter’s calls to PersistMemory.

[ { "id": "client-example", "spaceId": "space_your_client", "apiKeyEnv": "CLIENT_EXAMPLE_API_KEY", "tokenEnv": "CLIENT_EXAMPLE_TOKEN" } ]

Ordinary account keys (pm_live_) have account-wide API permissions. Naming an ordinary key after a client does not restrict it to that client’s Space. Keep account keys on infrastructure you control. Client Space keys (pm_space_) enforce the single-Space REST restrictions above and can be stored on the authorized client’s backend. Keep either kind in a server secret store, never a browser bundle, URL or log.

Authenticate and authorize the caller in your application. Resolve their client-to-Space mapping from your database and include that exact Space in every capture and retrieval. Never trust a customer-posted spaceId, client ID, or conversation ID as authorization. Refuse requests with missing mappings or failed access checks; do not retry without a Space.

The example below uses the TypeScript SDK with a Capture + read Client Space key for one client’s backend. Implement the two application adapters in ./your-server for your own authentication, authorization, and approved source data. Use a separately configured key for each client. Store returned job IDs against the same client and check that association before exposing job status.

npm install @persistmemory/sdk

import { PersistMemory } from "@persistmemory/sdk"; // Application adapters you must implement: authenticate staff, // authorize their client, then read that client's approved material. import { requireStaffClient, loadApprovedProcedure } from "./your-server"; // Configure this backend for one client with its own Client Space key. const apiKey = process.env.PERSISTMEMORY_CLIENT_SPACE_KEY; if (!apiKey?.startsWith("pm_space_")) throw new Error("Missing Client Space key"); const baseUrl = "https://api.persistmemory.com"; const memory = new PersistMemory({ apiKey, baseUrl }); async function verifySpace(spaceId: string, capability: string) { // No SDK resource is needed for the key's limited self-description. const response = await fetch(baseUrl + "/api/v1/client-space", { headers: { authorization: "Bearer " + apiKey }, cache: "no-store", signal: AbortSignal.timeout(10_000) }); if (!response.ok) throw new Error("Client Space key could not be verified"); const info = await response.json(); if (info.space?.id !== spaceId || !Array.isArray(info.capabilities) || !info.capabilities.includes(capability)) { throw new Error("Key does not match the configured Space or permission"); } } export async function captureProcedure(request: Request) { const client = await requireStaffClient(request); // Read this mapping from your server database, never request JSON. const spaceId = client.persistMemorySpaceId; if (!spaceId) throw new Error("Client has no configured Space"); await verifySpace(spaceId, "capture:text"); const procedure = await loadApprovedProcedure(client.id); const accepted = await memory.memories.remember({ text: procedure.text, title: procedure.title, spaceIds: [spaceId] }, { idempotencyKey: "client:" + client.id + ":procedure:" + procedure.id + ":v" + procedure.version }); // Store this association on your server so job access is client-checked. // accepted.jobId is a processing job, not a memory ID. return accepted; } export async function recallProcedures(request: Request, query: string) { const client = await requireStaffClient(request); const spaceId = client.persistMemorySpaceId; if (!spaceId) throw new Error("Client has no configured Space"); await verifySpace(spaceId, "search"); return memory.search.query({ query, spaceIds: [spaceId], limit: 5 }); }

Use a stable idempotency key for the same source revision and client. Change it when approved source content changes. Search returns ranked memories and diagnostics, not a generated answer. Use those records as evidence in your own staff interface, or use the Space’s Ask page to test answers.

Acceptance is the start of processing

memory.memories.remember calls POST /api/v1/remember with { text, title, spaceIds: [spaceId] }. It returns HTTP 202 with jobId, not a memory. Poll memory.jobs.get(jobId) with a bounded retry policy, or check later from a background worker. A Client Space key can read only jobs and documents whose Space selection is exactly its bound Space; account jobs and results associated with multiple Spaces are denied.

// Use a job ID stored for this authorized client, not an unchecked // jobId supplied by a customer. Check on a later request or poll with // a fixed limit; do not keep a browser request waiting forever. const job = await memory.jobs.get(storedJobId); switch (job.status) { case "completed": // Review the saved material, then test scoped retrieval. break; case "failed": case "dead": // Surface job.error; review before resubmitting material. break; case "queued": case "running": // Still processing. Check again later. break; }

When using the starter, POST /v1/captures returns jobId, receipt, statusPath and resultPath. Read the returned resultPath using GET /v1/jobs/:id/result on your starter, with the same client Authorization token and X-Capture-Receipt header used for status. The starter derives the document from that verified job; callers cannot choose another document. Keep the receipt out of URLs and logs.

// These are routes on your integration starter, not the PersistMemory API. // Keep the capture response and receipt with the authorized client's record. const response = await fetch(starterOrigin + capture.resultPath, { headers: { authorization: "Bearer " + clientToken, "X-Capture-Receipt": capture.receipt } }); if (!response.ok) throw new Error("Capture result could not be read"); const result = await response.json(); // result.resultStatus: available, pending, or unavailable. // available means an outcome or recognized text can be reviewed. // Only an explicit document.outcome.memoryCount === 0 means zero saved memories. // Missing outcome data is unknown; it must not be rendered as zero. // Poll pending with bounds. Surface unavailable for operator review.

The starter’s resultStatus is available, pending or unavailable. Available means an outcome or recognized text is present for review; it does not by itself mean extraction finished or memories were saved. Pending can be checked later with bounded polling. A terminal job with no usable result is unavailable and needs operator review, rather than indefinite polling. Only an explicit document.outcome.memoryCount === 0 reports zero saved memories. Missing outcome data is unknown, never an inferred zero.

Client Space keys cannot call memory.memories.uploadFile. For approved file ingestion, use the signed-in dashboard or an ordinary account key with the required permissions and an explicit Space selection. Do not substitute an account key in a client integration to bypass a denied route.

Keep the client boundary explicit

A Space is a knowledge boundary inside an account. A memory may be filed in multiple Spaces, so do not file one client’s material into another’s Space or merge client Spaces. Sharing gives an accepted collaborator access to that Space; it does not create a separate tenant account.

Client Space keys do not grant conversations or live tools. When testing through the signed-in dashboard, start a new conversation for each client context. Account connections and live tools may have broader access than a single Space: a selected Space limits saved-memory retrieval, not the authority of an account’s Gmail, Drive, or computer tools. Use only approved sources and tools for the pilot.

A restricted key does not provision a native POS or booking integration, staff identity, client billing, quotas or a complete tenant platform. Those remain responsibilities of your application.

Client Spaces · restaurant and gym pilots - PersistMemory