# past.dev developer documentation Two programmatic interfaces to the same engine. - **MCP Server**: Two authenticated MCP servers. Members administer the organization from an assistant, under their own role. Your end users reach a project's memory from theirs, signed in at your own identity provider. - **Memory API**: Send timestamped text and retrieve ranked, source-backed memory for your application. To get an API key, sign up at https://sso.past.dev/sign-up, then create a project key in the console. Checked against the running services on September 18, 2026. --- # past.dev MCP Server # What the past.dev MCP servers are > past.dev operates two authenticated MCP servers: one for the people who administer an organization, and one for your product's end users. Product: past.dev MCP Server. Source: https://past.dev/docs/mcp/overview MCP, the Model Context Protocol, is an open standard that connects AI assistants to data and tools. past.dev operates two authenticated MCP servers. They share a host and nothing else: each one signs a different person in, and each one carries its own tools. | Server | Who signs in, and what it carries | | --- | --- | | **Account server** | `https://app.past.dev/mcp`. A console member administers the organization from an assistant: projects, project keys, members, access, plan, usage, the request log and the debugging reads. 49 tools, filtered by the caller's platform role. Section 02 connects it and section 03 lists every tool. | | **End-user server** | `https://api.past.dev/mcp/`, one per project. Your own end users reach that project's memory from their own assistant. They sign in at your identity provider and never hold a past.dev account. Four tools. Section 04 describes it. | | **Documentation server** | `https://past.dev/mcp`. This documentation, read-only and unauthenticated. Use it while you write code against the API. It reaches no account data. | ### Which server you want - **You administer past.dev itself.** Connect the account server. It does what the console does, as you, under your role. - **Your users need their own memory.** Enable the end-user server on the project. Each user signs in at your identity provider and recalls only what their identity may see. - **You are writing integration code.** Add the documentation server, and call the [Memory API](/docs/memory-api/overview) for ingestion and recall from your own software. > **The account server recalls nothing** > > The account server administers the organization. It runs no recall and writes no memory content. Its debugging tools read what the console's Trace screen shows about what you sent: a memory's headline, the text of the data point it came from, and how one came from the other. > > Memory itself reaches an assistant two ways: the end-user server on a project, described in section 04, or the Memory API called from your own software. ### What connects to it Both servers work with clients that support remote MCP servers over Streamable HTTP with OAuth. These include Claude desktop, Claude web, Claude Code, Cursor, VS Code with GitHub Copilot, Windsurf and Zed. Section 02 gives the setup for each one. > **Claude setup** > > past.dev is **not yet listed in Claude's connector directory**. Add it manually as a custom connector. Manual setup uses the same OAuth flow and the same security controls. # Connect an MCP client > Add the account server URL to your client, then sign in through your browser. No API key is required. Product: past.dev MCP Server. Source: https://past.dev/docs/mcp/quickstart This page connects the **account server**. To connect an end user to a project's memory instead, read section 04. ### What you need - A past.dev account with an accepted organization membership. - An MCP-capable client (see below). - The client configuration described below. OAuth does not require you to generate or paste a token. ### The address - **Server endpoint**: `https://app.past.dev/mcp` - **Server name**: `past` - **Transport**: `Streamable HTTP` > **Three addresses, three servers** > > `https://app.past.dev/mcp` is the account server. `https://past.dev/mcp` serves this documentation. `https://api.past.dev/mcp/` is a project's end-user server. ### Claude, desktop and web 1. Open **Settings → Connectors**. 2. Choose **Add custom connector**. 3. Name it `past` and paste `https://app.past.dev/mcp` as the URL. 4. Click **Connect**. A browser window opens on past.dev's sign-in page. 5. Sign in and approve the connection. Leave the client ID and secret fields under Advanced settings empty. The server registers the client itself. ### Claude Code ``` claude mcp add --transport http past https://app.past.dev/mcp ``` Run `/mcp` inside Claude Code and select **Authenticate**. Complete the sign-in in the browser. Add `--scope user` to make the server available in every project. Without it, the server is available only in the current project. ### Cursor Add it in one click: [Add to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=past&config=eyJ1cmwiOiJodHRwczovL2FwcC5wYXN0LmRldi9tY3AifQ%3D%3D). Cursor asks you to confirm, then writes the entry below for you. Or add this to `~/.cursor/mcp.json` for all projects, or `.cursor/mcp.json` inside one project: ```json { "mcpServers": { "past": { "url": "https://app.past.dev/mcp" } } } ``` Cursor prompts you to sign in the first time it connects. ### VS Code (GitHub Copilot) Add it in one click: [Add to VS Code](vscode:mcp/install?%7B%22name%22%3A%22past%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fapp.past.dev%2Fmcp%22%7D). VS Code asks you to confirm, then adds the server. Or add this to `.vscode/mcp.json` in your workspace: ```json { "servers": { "past": { "type": "http", "url": "https://app.past.dev/mcp" } } } ``` ### Any other client Most MCP clients require these settings: | Setting | Value | | --- | --- | | URL | `https://app.past.dev/mcp` | | Transport | Streamable HTTP | | Authentication | OAuth 2.1, discovered automatically | If the client asks for an API key or a bearer token, check its authentication setting. This server uses OAuth and refuses a manually configured authorization header. ### Verify the connection Ask the assistant who it is signed in as. It calls `get_viewer`, which returns your name, your platform role and the projects you reach. That call is the fastest proof that the connection works. The tool list your client receives is filtered by your platform role, so two members of the same organization can see different lists. Section 03 gives the complete catalogue. To verify that the server is reachable before you sign in: ``` curl https://api.past.dev/.well-known/oauth-protected-resource/mcp ``` A healthy server returns its resource identifier and the address of the sign-in service: ```json { "resource": "https://app.past.dev/mcp", "authorization_servers": ["https://sso.past.dev"], "bearer_methods_supported": ["header"] } ``` # Available tools > The account server carries 49 tools. Each one does what a console screen does, under your platform role and your reach on the project you name. Product: past.dev MCP Server. Source: https://past.dev/docs/mcp/tools Every tool mirrors a console action. The server resolves your platform role and your reach on the named project for each call, so an assistant can never do more than you can do in the console. A tool your role does not hold is **absent from the tool list**. Calling an absent tool returns `unknown tool`, the same answer a tool that does not exist returns, so the list cannot be used to map what other roles can do. ### Naming a project Project-scoped tools name their project by **slug**, the immutable name shown next to every key. Where the organization has exactly one live project, the argument may be omitted. Omitted with several live projects, the call is refused `project-required`. A project you do not reach and a project that does not exist are both **not found**. ### Organization | Tool | Kind | What it does | | --- | --- | --- | | `get_viewer` | read | Who you act for: name, email, platform role, the organization, and every live project you reach with the level you reach it at. The first call an assistant should make. **Everyone.** | | `get_organization` | read | The organization's name, slug, plan, creation date and member count. Mirrors Settings › General. **Everyone.** | | `list_tags` | read | The organization's tag vocabulary, with how many projects carry each tag and how many rules match it. **Owner, Admin.** | | `create_tag` | write | Adds a tag to the vocabulary. Lowercase letters, numbers and dashes. **Owner, Admin.** | | `rename_tag` | write | Renames a tag. The projects carrying it and the rules matching it follow the new name, so nobody's access moves. **Owner, Admin.** | | `merge_tags` | write | Merges one tag into another: the projects carrying it carry the other, and the rules matching it match the other. Destructive, so it asks for `confirm`. **Owner, Admin.** | | `delete_tag` | write | Deletes a tag nothing depends on. A tag a project carries or a rule matches is refused. Destructive, so it asks for `confirm`. **Owner, Admin.** | ### Projects and keys | Tool | Kind | What it does | | --- | --- | --- | | `list_projects` | read | The live projects you reach: slug, display name, tags, monthly caps, memory count and your level. `includeArchived` adds archived projects, for Owner and Admin. **Project Read.** | | `get_project` | read | One project in full, as the Settings › Projects drawer shows it, including who reaches it. **Project Read.** | | `create_project` | write | Creates a project. The slug is derived from the name as the console derives it, and the creator gets project Admin. **Member and above.** | | `update_project` | write | Renames a project and sets its tags. The slug never changes. **Project Admin.** | | `archive_project` | write | Archives a project: out of the switcher, and every call on its keys refused. **Project Admin.** | | `unarchive_project` | write | Brings an archived project back into the switcher and back onto its keys. **Project Admin.** | | `delete_project` | write | Deletes a project permanently and revokes every key it holds. Destructive, so it asks for `confirm`. **Project Admin.** | | `set_project_caps` | write | Sets the project's monthly ingestion cap and monthly credit cap. **Project Admin.** | | `list_api_keys` | read | A project's live keys, newest first: id, name, masked value, created by, created and last used. Never the clear value of a key. **Project Admin.** | | `create_api_key` | write | Mints a project key, attributed to you under Created by. The answer carries the clear value once, and the assistant is told to hand it to you and put it nowhere else. **Project Admin.** | | `revoke_api_key` | write | Revokes a project key. It stops working on its next call and cannot be restored. Destructive, so it asks for `confirm`. **Project Admin.** | | `get_mcp_server` | read | A project's end-user MCP server, as Build › MCP server shows it: on or off, its address (the address it would take while it is off), the login connection, the live access links, and whether anybody can sign in yet. **Project Admin.** | | `enable_mcp_server` | write | Turns a project's end-user MCP server on and answers with its address. There is nothing to choose: the address comes from the organization and project slugs. **Project Admin.** | | `create_mcp_access_link` | write | Mints an access link for one identity, optionally created with its traits. The link is the credential, so the answer carries it once and the assistant is told to hand it to you and put it nowhere else. The server must be on. **Project Admin.** | | `get_project_access` | read | Who reaches one project: each person with their level, where it comes from (role, rule or grant), and until when. **Project Read.** | | `grant_project_access` | write | Grants one member direct access to one project at Read, Write or Admin, optionally until a date. Bounded by what you hold yourself. **Project Admin.** | | `revoke_project_grant` | write | Withdraws one member's direct grant. Access they hold through their role or a rule stays. Destructive, so it asks for `confirm`. **Project Admin.** | ### Members and access | Tool | Kind | What it does | | --- | --- | --- | | `list_members` | read | Everyone in the organization: userId, name, email, platform role, joined date, the projects they reach, and pending invitations. **Owner, Admin.** | | `invite_member` | write | Invites an email address at a platform role, with you as the inviter. `Owner` is refused, as on the screen. **Owner, Admin.** | | `resend_invitation` | write | Sends a pending invitation again with a fresh link. The old link stops working. **Owner, Admin.** | | `set_member_role` | write | Changes a member's platform role. Promoting to Owner transfers ownership. Destructive, so it asks for `confirm`. **Owner, Admin.** | | `remove_member` | write | Removes a member, or revokes a pending invitation. The last Owner is refused. It also ends every account MCP session that person holds. Destructive, so it asks for `confirm`. **Owner, Admin.** | | `list_access_rules` | read | The organization's tag-matched access rules: who each names, the tags and how they match, the level it gives, and when it lapses. **Owner, Admin.** | | `list_access_groups` | read | The organization's groups, their members, and how many rules name each one. **Owner, Admin.** | | `preview_access_rule` | read | Which live projects and which people a rule would match if it were saved as described. Nothing is written. **Owner, Admin.** | | `create_access_rule` | write | Creates a tag-matched rule: the groups and people it names reach every live project its tags match, at its level, until it lapses. **Owner, Admin.** | | `update_access_rule` | write | Replaces a rule with what you send, as saving the rule drawer does. Send every group, person and tag it should keep. **Owner, Admin.** | | `delete_access_rule` | write | Deletes a rule. The people it reached lose that access unless a role, another rule or a grant gives it. Destructive, so it asks for `confirm`. **Owner, Admin.** | | `create_access_group` | write | Creates a group of members that rules can name. A group grants nothing until a rule names it. **Owner, Admin.** | | `update_access_group` | write | Renames a group or sets its members. Sent members replace the whole membership. **Owner, Admin.** | | `delete_access_group` | write | Deletes a group. `Everyone` cannot be deleted, and neither can a group a rule names. Destructive, so it asks for `confirm`. **Owner, Admin.** | ### Plan, usage and requests | Tool | Kind | What it does | | --- | --- | --- | | `get_plan` | read | The plan: the credits it includes, how many content bytes buy a credit, what a recall costs, how long a bought credit lasts, and what happens at zero. **Everyone.** | | `get_usage` | read | This billing period's usage in UTC: credits spent against the credits included, split by what spent them, per project, with the reset date. **Owner, Admin, Billing.** | | `list_requests` | read | One project's request log, newest first, with the Requests screen's filters, search and date range, paged by cursor. **Project Read.** | | `get_request` | read | One call from the request log, as the Requests detail drawer shows it: method, path, status, the request id and the payload. **Project Read.** | | `request_histogram` | read | Calls per time bucket over a range for one project, split by how they ended: accepted, accepted then failed later, and rejected. **Project Read.** | | `get_management_key_activity` | read | What the organization's management key called in the last 30 days: each route with its method, call count and last call. **Owner, Admin.** | ### Debugging These tools answer the questions an integration raises: where a send stopped, what a memory was built from, what an import will cost, and what is left to spend. They read what the console's Requests, Trace and Usage screens read, and none of them leaves a row in the request log, so listing your requests shows what your application sent and not what your assistant asked. Only `retry_ingestion` writes. | Tool | Kind | What it does | | --- | --- | --- | | `explain_ingestion` | read | Where one ingestion stopped and what to do about it: its phase, why it stopped and whether a retry can fix it, each data point it carried with whether it is searchable and how many memories came out of it, and one sentence naming the next step. Name it by its ingestion id or by the request id that carried it. Start here when data does not show up. **Project Read.** | | `retry_ingestion` | write | Runs an ingestion that failed again, under the same ingestion id, as Retry in the console's request drawer does. It spends no credits. Only a failed ingestion can be retried, and `explain_ingestion` says whether a retry can fix it. Asks for `confirm`. **Project Write.** | | `find_trace_roots` | read | Memories and sources matching a search, newest first, in two paged groups: a source matches on its text and its data point id, a memory on its own words. Pass an ingestion id to list what one send carried. Each entry's id is what the lineage and timeline tools take. **Project Read.** | | `get_trace_node` | read | One memory or source by its id: its headline, its data point and revision, when it occurred, arrived and became searchable, whether it is current, and how many inputs and outputs it has. A source carries its text, cut to 4,000 characters. **Project Read.** | | `get_lineage` | read | What a memory was built from and what was built on it: the nodes around a root and the edges among them, where every edge reads as built from. This is the answer to "the system claims this, where did it come from". **Project Read.** | | `get_timeline` | read | The same work on a clock: every node around a root with when it occurred, arrived and became searchable, the sends in the window that carried its sources, the recalls that returned it, and which memory replaced which. **Project Read.** | | `estimate_ingestion` | read | Cuts files into the data points past reads best and prices them, before anything is sent: each file detected as email, chat, transcript, table, records, code, documentation or notes, the points, the batches to send them in, the credits, and how many points fit what is left. Writes nothing and spends nothing. **Project Read.** | | `get_credit_balance` | read | The credits left, what the month included and bought with the day the bought ones lapse, the reset date, and one project's month: credits spent, ingestions, recalls, and what is left of its caps. The answer to a send refused with `402 out-of-credits` or `429 project-cap-reached`. **Project Read.** | ### Destructive tools ask first Every tool marked destructive above takes a `confirm` argument. A call without `confirm: true` **does nothing**. It answers with one sentence describing what the call would do, and the assistant is expected to put that sentence to you before it calls again. An unconfirmed call that the confirmed call would refuse answers the refusal instead of the sentence. An Admin who asks to make someone Owner, or the only Owner who asks to remove themselves, gets the refusal, so nobody is asked to agree to a change that will not happen. > **A minted key or link is a secret** > > `create_api_key` is not destructive, but it mints a project key and the clear value is in the result. The tool description tells the assistant to hand the value back to you and put it nowhere else. Store it in your secret store, and revoke it with `revoke_api_key` if it reaches a transcript you do not control. > > `create_mcp_access_link` is the same: the link in the result reads and writes as its identity. Revoke it on **Build › MCP server** if it reaches a transcript you do not control. ### What no tool does No tool reveals an existing key or access link, adds a login connection (it carries your identity provider's client secret), rotates the management key, changes the payment method, renames or deletes the organization, or transfers ownership except through `set_member_role`. Each of those is a screen action with a confirmation you read yourself. An assistant that needs one tells you where the button is. **Reading and writing memory.** The account server recalls nothing and writes no memory content: its debugging tools only read what the console shows about what you sent. Your users reach a project's memory through the end-user server, and your own software reaches it through the Memory API. Memory API reference: /docs/memory-api/api-reference # Use past.dev with your product's users > Enable the end-user MCP server on a project. Your users reach that project's memory from their own assistant, signed in at your identity provider. Product: past.dev MCP Server. Source: https://past.dev/docs/mcp/serving-your-own-users A project can offer its own MCP server to **your** end users. A person signs in at the identity provider you already run, and every tool call answers as the identity they resolved to: project-visible memory plus the audiences that identity is in, and nothing else. past.dev is the authorization server and never a place to sign in. Your users never see a past.dev account, and past.dev never holds a password for them. You register one OAuth application at your provider and paste its credentials into the console. ### The address Enabling the server for a project sets its **handle**: the organization slug and the project slug, joined by a dash. There is nothing to type. Once enabled the handle never changes, even if the project is renamed, because your users have already installed it. - **Server endpoint**: `https://api.past.dev/mcp/` - **Example**: `https://api.past.dev/mcp/acme-support` - **Transport**: `Streamable HTTP` - **Auth**: `OAuth 2.1 · PKCE · your identity provider` The address is the canonical resource URI: the `resource` parameter clients send, the audience of every token, and the issuer of the authorization server. A request under an unknown handle is not found. In self-host the address is your own host, with no third party involved. ### Turning it on 1. Open **Build › MCP server** on the project in the console. 2. Connect an identity provider for the project, and paste the credentials of the OAuth application you registered there. 3. Press **Enable MCP server**. The handle and the address appear, with a *Copy* button and the install snippet for each client. 4. Decide who may use it: everyone the provider signs in, or only identities matching a rule over their traits. 5. Decide whether `remember` is offered, with the project's **write toggle**. ### Which identity providers Five presets are available to every project. A preset fills in the scopes, the join key and the claim mapping. You supply the OIDC application you created at the provider: its issuer, its client id and its client secret. The secret is stored encrypted and is never shown again. | Provider | Groups, and directory sync | | --- | --- | | **Microsoft Entra ID** | Groups arrive in the ID token as object IDs. Directory sync available. | | **Okta** | Groups arrive in the ID token. Directory sync available. | | **Google Workspace** | Google never puts groups in an ID token. A rule over groups here reads them from the directory sync, which makes the sync the way to get them. | | **JumpCloud** | Groups arrive in the ID token, under the attribute name you give the application. Directory sync available. | | **OneLogin** | Groups arrive in the ID token. No directory sync, so a rule can name only the groups the token carries. | **Auth0**, **Clerk**, **Amazon Cognito** and any OpenID Connect provider that publishes a discovery document are opened per organization on request. Those four default to the `sub` claim as the join key, where the five above default to `email`. Ask your past.dev contact to open one. The join key is the claim whose value becomes the identity's `user_id`, so it is the claim that decides when two sign-ins are the same person. A connection also decides what happens to a person the directory has not seen: create an identity for them, which is the default, or refuse them. ### Directory sync Sign-in brings identities in one at a time, at the moment each person first arrives. Directory sync brings every user and group in ahead of any sign-in, so an audience is populated before its people have connected anything. It runs through Nango, every 48 hours, with a **Sync now** button on the connection for the times you want it sooner. The sync writes the same traits a sign-in writes, under its own provenance, and marks a user the provider suspended or deactivated. The connection row reports the last run: its time, the user and group counts, what changed, and when the next run is due. > **One connection per project** > > A project binds to one provider, which is what sends a person straight to their sign-in page with no chooser in between. > > Turning a connection off refuses sign-ins at once and stops its tokens from refreshing. Everything it wrote stays. ### The tools | Tool | Kind | What it does | | --- | --- | --- | | `recall` | read | `query`, optional `limit`. The evidence this identity may see, with its sources. The same result `POST /api/v1/recall` gives for that identity. Scope `memory:read`. | | `answer` | agent | `question`. A grounded answer over the same evidence, with its citations, or an abstention when the evidence does not support one. Scope `memory:read`. | | `remember` | write | `content`, optional `label`. Ingests a data point scoped to the identity's own private audience, so only this identity recalls it. Offered while the project's write toggle is on. Scope `memory:write`. | | `who_am_i` | read | The identity this session resolved to, its traits, the audiences it reaches, and the connection, link and client the session came through. Scope `identity:read`. | The server advertises `memory:read` and `identity:read`, plus `memory:write` while the write toggle is on. Consent lists the tools those scopes reach, and the token carries the consented set. Turning the write toggle off refuses `remember` on existing tokens with `insufficient_scope`, and the tool leaves the client on its next tool listing. > **What a user remembers is theirs** > > `remember` writes through the ordinary ingest path, with the audience set to the identity's own private audience. The request log, the pipeline and retention all see an ordinary data point. It is never widened to the project. ### Signing in A project has exactly one login connection, so the authorization request redirects straight to it. There is no chooser. Two pages are past.dev's own, rendered outside the console and carrying your project's display name: - **Consent**: who the person resolved to, the client's name and redirect host, and the tools it will get, as a plain list. It is remembered per client and identity for as long as the session lives, so a second install of the same client skips it. - **Refused**: one sentence for the reason: the server is off, no login connection is enabled, the connection refuses people it has not seen, or the identity does not match the access rule. The page never says whether an id exists, and never says which trait fell short. A person the directory has not seen gets an identity at first sign-in. Signing in refreshes that identity's traits through the connection's claim mapping, so a group change at your provider changes access at the person's next sign-in with nobody touching the console. ### Deciding who may connect Access is a **rule over traits** on the connection. One rule decides who may connect, so there is no switch to set per person. The default admits everyone your provider signs in. The alternative matches conditions of the form ` `, in the same grammar as an audience rule, so `idp_group is one of [support, leadership]` reads the same in both places. The rule is evaluated at sign-in, at every refresh and on every tool call, against the identity's current traits. An identity that stops matching is refused at its next sign-in, its refresh is refused, and its access token runs out within the hour. ### Agents and services: access links An agent, a bot or a service has no browser and no account at your provider. For those, the console mints an **access link**: one address that is one identity. ``` https://api.past.dev/mcp//link/ ``` The client installs the link as its MCP server URL and nothing else. past.dev answers it with no challenge, because the secret in the path is the credential, and every call runs as that identity with its audiences. The four tools work over a link exactly as over a token, and `who_am_i` names the link. > **A link travels in configuration and logs** > > That is the trade the one-line install makes. Mint one link per install, so revoking one agent leaves the others running. Revocation takes effect on the link's next call. The access rule does not filter links, so a leaked link is revoked to end it. ### Or call the Memory API yourself If your product already has its own MCP server, add tools to it that call the Memory API. Your server authenticates the user, picks the project key, and passes the user's identity to `/recall`. ```bash # Inside your own MCP tool handler, after you have authenticated the user. # The key names the project; there is no projectId field. curl -X POST https://api.past.dev/api/v1/recall \ -H "Authorization: Bearer $PAST_API_KEY" \ -d '{ "query": "what is the Acme pilot budget?", "identity": "demo-user" }' ``` > **Keep the key on your server** > > A past.dev project key reaches everything in its project. Keep it on your server. Never send it to an MCP client, a browser or a user device. > > Recall requires an `identity` and returns project-visible memory plus memory visible to that identity. When you call the API yourself, that mapping is the security boundary and it stays in your code. The end-user server above is the version where past.dev holds that boundary instead. **Memory API endpoints.** The Memory API reference documents the request and response fields for ingestion and recall. Memory API reference: /docs/memory-api/api-reference # How it works > Both servers verify identity and permissions on every request. The client receives only the tools the caller holds at that moment. Product: past.dev MCP Server. Source: https://past.dev/docs/mcp/how-it-works ### Signing in to the account server On past.dev's cloud, sign-in uses **WorkOS AuthKit** at `sso.past.dev`, the same identity service the console uses. On a self-hosted deployment the deployment itself is the authorization server: it hosts the consent screen and issues the tokens, so a self-hoster gets the same server with no account on any third-party service. The client completes these steps itself: 1. **Discover the authorization server** The client opens the endpoint with no credential and receives `401` with a `WWW-Authenticate` challenge carrying `resource_metadata`. RFC 9728 metadata names the authorization server, and RFC 8414 metadata for that server is served at the resource origin as well, so a client that knows only the MCP URL completes the flow. 2. **Register the client** Dynamic Client Registration assigns an identity to the client, so no administrator provisions it in advance. Redirect URIs are exact-match and must be HTTPS or loopback. 3. **Sign in and approve access** The browser runs the authorization code flow with PKCE. S256 is required. You enter your password on past.dev's own sign-in page. The AI client never receives it. 4. **Receive a short-lived access token** The audience binds the token to the MCP address it was issued for. It reaches no other past.dev API, and a token issued for another address is refused. A refresh token renews the connection without another sign-in. 5. **Send the token with each request** The server validates the token and recomputes the caller's permissions for every request. ### What happens on each request The transport is stateless. The server keeps no authorization session between calls, and performs these checks every time: | Step | What is checked | | --- | --- | | **Validate the token** | The signature is verified against the published JWKS keys, along with issuer, audience and expiry. A failed check returns `401` with a reference to the discovery document, so the client can authenticate again. | | **Resolve who you are** | The token carries an opaque identifier and no email, role or permission. The server maps the identifier to a record in its own database. | | **Confirm you still belong** | The account server verifies an accepted organization membership. After a removal the next call fails, however long the token still has to live. A pending invitation to the same address does not reopen an old token. | | **Compute your permissions** | Permissions come from the platform role and the project reach held in the database. Permission claims inside a token are never used. | | **Filter the tool list** | A tool whose scope the caller lacks is removed from the list. Because the list is derived per request, a role change appears on the assistant's next tool listing with no reconnect. | ### Tool permissions Each account tool carries one scope. The scope table maps each platform role to the scopes it holds. Holding a scope allows a family of tools at all; reach inside a project is still resolved on every call, so a Member holding the project scope administers only the projects they reach, at the level each tool needs. Authorization **fails closed**. A tool without a declared scope is never exposed. A request with no resolved caller receives an empty tool list. A new tool stays unavailable until a role holds its scope. ### What the server tells the assistant The account server's handshake carries instructions: that every action runs as the signed-in person, that an absent tool means a missing permission to report rather than work around, that destructive tools ask for `confirm`, and that slugs and ids are used exactly as another tool returned them. Each tool description names the console screen it mirrors and the role it needs, so an assistant can explain a refusal in the words you will find on the screen. ### Rate limits The account server carries one rate budget per person, shared by every token and every client that person connected. It counts every request, including tool listings and the handshake. Past the budget a request answers `429` with `Retry-After` and leaves no record. A request with no valid token is answered `401`, never `429`, because the budget belongs to the person a token names. ### Where the server runs Both servers run inside the main past.dev API, behind the same TLS termination, network protection and infrastructure. Neither retains conversation state between calls, so any instance can serve any request. Deployments and restarts do not change a connection's security settings. # Security > Short-lived OAuth tokens, permissions derived from server-side records on every request, and one tenant per caller. Product: past.dev MCP Server. Source: https://past.dev/docs/mcp/security ### Credentials - **No API keys.** An MCP connection does not use an API key. A client configured to ask for one is using the wrong authentication method. The one exception is an end-user access link, where the secret in the URL is the credential and section 04 states the trade. - **Short-lived tokens.** Access tokens expire within the hour and refresh automatically. A token works only against the address it was issued for. - **Audience binding.** A token minted for one MCP address is refused everywhere else, and a token minted for another past.dev service is refused here. A token for another project's handle is refused, never a cross-project read. - **PKCE is mandatory.** S256 is the only accepted challenge method. It protects the authorization code during the redirect. - **Your password never reaches the AI client.** Authentication happens in your own browser, on past.dev or on your own identity provider. - **Refresh tokens rotate.** Each use issues a new pair, and a refresh naming a different client is refused. - **Revocation applies on the next request.** Signing out from the client ends the refresh token at once. The servers have no offline mode. - **Sign-in policies apply.** The account server uses the same hosted login as the console, so the policies enforced there apply to it. The end-user server uses your identity provider, so your policies apply there. - **Membership is checked per request.** A removed member cannot make another account call, even with an unexpired token. Removing a member also ends every account MCP session that person holds. ### Authorization uses server-side records The access token establishes identity and nothing else. For each call past.dev evaluates the caller's current organization, platform role, project reach and audiences. Permission claims inside a token cannot widen access, because the server does not read them. A client or a prompt can call only the tools exposed for the caller's current server-side permissions. ### Tenant isolation The organization is derived from the caller's record. Organization and user identifiers are **not tool parameters**, so a prompt cannot redirect a call to another organization through one of these fields. On the end-user server the project comes from the handle in the address, and the identity comes from the token. Nothing on the token, the link or the client widens what that identity sees. Inside your own organization, the same rules the console enforces apply again at every tool: - Results are filtered to the caller's current visibility before they are returned. - Writes run the same validation and authorization as the console action they mirror. - Record identifiers are opaque and cannot be enumerated as a sequence. ### Transport and infrastructure - HTTPS only, HTTP/2, behind Cloudflare edge protection. - Session identifiers returned to clients are opaque and validated by the service. - An authentication failure never echoes the submitted token, and a refusal names nothing about the account. ### Data sent to the AI client > **External data flow** > > **The assistant's model receives tool results.** Anything a tool returns can be sent to the AI client and to its model provider. On the account server that is account administration data. On the end-user server it is the memory that identity may see. > > Include this flow in your data map, and review the provider's data-handling terms. You select and contract with that provider separately from past.dev. # Administrative controls > What an administrator controls on each server, and where that control lives in the console. Product: past.dev MCP Server. Source: https://past.dev/docs/mcp/admin-controls The two servers are administered differently. The account server is on for every organization and bounded by each person's role. The end-user server is off until someone enables it on a project, and everything about it is a console setting. ### What you control today | Control | Status | | --- | --- | | Turning the end-user server on and off | **available** **Build › MCP server**, per project. Disabling it fails every install closed rather than routing it elsewhere. The account server has no such switch: see the next table. | | Deciding which of your users may connect | **available** A rule over traits on the project's identity connection. It is evaluated at sign-in, at every refresh and on every tool call, so a group change at your provider takes effect without anyone touching the console. | | Restricting the end-user server to read-only | **available** The project's write toggle. Turning it off refuses `remember` on existing tokens and drops the tool from every client on its next tool listing. | | Authentication policy | **available** The account server inherits the console's hosted login, so the policies enforced there apply. The end-user server uses your own identity provider, so your policies apply and past.dev never holds your users' passwords. | | Cutting off one person's access | **available** For a member, remove the organization membership: the next account call fails, and every account MCP session that person holds ends. For one of your users, change the trait their access depends on, or revoke their access link. | | Bounding what a person can reach | **available** Through the platform role and project reach for a member, and through audiences for one of your users. MCP grants no access the caller does not already hold. | | Seeing what an assistant did | **available** Every account tool call is written to the organization's request log with method `MCP`, the tool name as its path, the member as its caller and the client it came from. **Settings › MCP** reads those rows as Recent activity. A refused call is a row too, with its error code. | | Revoking one MCP connection | **available** For one of your users, revoke the access link or sign the client out. For a member, removing the membership ends every session. A per-connection revoke screen for members is in the next table. | ### If you are evaluating for an enterprise rollout Both servers use the platform's existing isolation, least-privilege and credential controls. Administrators manage member access through roles and memberships, and end-user access through the identity provider and the traits it maps. For a controlled pilot, enable the end-user server on one project, set its access rule to a single group at your provider, and leave the write toggle off. Ask your past.dev contact about any control this page does not list. # Privacy and compliance > The MCP servers use the same security and privacy controls as the rest of past.dev. Product: past.dev MCP Server. Source: https://past.dev/docs/mcp/privacy-compliance | Certification | What it covers | | --- | --- | | **ISO/IEC 27001** | Information security management. Independently certified controls covering access control, cryptography, secure development, supplier management, logging and incident response. | | **ISO/IEC 27701** | Privacy information management, extending 27001. Covers how personal data is processed, minimised, retained and handled when a data subject exercises their rights. | | **SOC 2 Type II** | Independent audit of control operation over a defined review period. | Neither server adds a data store, an identity system or a permission model. Both use the same database, access rules, infrastructure and audit scope as the rest of past.dev. ### What a reviewer usually asks | Question | Answer | | --- | --- | | Does it create a new data store? | No. Reads and writes go to the existing organization data. | | Are there standing credentials? | No. OAuth 2.1 with short-lived tokens. The one exception is an end-user access link for an agent, which section 04 states plainly. | | Can it cross tenants? | No. The organization comes from the caller's record and the project from the handle in the address. Neither is a tool parameter. | | How is access revoked? | For a member, remove the membership: it is verified per request and also ends their MCP sessions. For one of your users, change the trait the access rule reads or revoke their link. See section 7. | | Can an admin disable it? | The end-user server, yes, per project, from Build › MCP server. The account server is on for every organization, and each member reaches only what their role reaches. | | Does our SSO apply? | For your own users, yes: they sign in at your identity provider and past.dev never holds their password. For members, the account server uses the same hosted login as the console. | | Is access least-privilege? | Yes. Per-tool scopes, role-derived, filtered per request, fail-closed, with project reach resolved on every call. | | Is there an audit log? | Every account tool call is recorded in the organization's request log and shown on Settings › MCP, including refusals. Section 7 covers what an administrator sees. | | Data residency and subprocessors | Same as the past.dev platform. Request the current subprocessor list and DPA from past.dev. | | Does data leave to a third party? | Yes. The AI client and its model provider receive tool results. You select and contract with that provider separately from past.dev. | ### Data minimisation in the tools themselves On the end-user server, an identity's audiences decide what `recall` and `answer` can return, and `remember` writes only to that identity's private audience. If you call the Memory API from your own MCP server instead: - Return only the data your application needs from the authorized recall page. - Derive the recall identity from your authenticated user, rather than letting the model choose it. - Keep project keys on your server and select them according to your own access policy. > **For your security team** > > Request current certificate numbers, audit periods, auditor details and the subprocessor list from the past.dev trust center or your past.dev contact. Certificate details change with each audit cycle. # Limits and known behaviour > Current behaviour and current product limitations. Product: past.dev MCP Server. Source: https://past.dev/docs/mcp/limits - **One organization at a time.** The account connection uses whichever organization is active for your account. There is no picker in the sign-in flow. Change it in the console, and the change applies from the next tool call. - **One login connection per project.** The end-user server redirects straight to it, so a project cannot offer a choice of identity providers. - **A handle never changes.** It is derived from the organization slug and the project slug when the server is enabled. Renaming the project does not move it, because your users have already installed the address. - **No prompts or resources.** Neither server exposes MCP prompts or resources. Tools only. - **No push notifications.** The transport is stateless, so a server cannot tell a client that something changed. Assistants find out by asking. - **Account tools follow the role.** Permissions come from the platform role and project reach, and the tool list is filtered per request. Section 7 covers the controls an administrator holds. - **Account MCP calls are not metered.** They are neither ingestions nor recalls. `recall` and `answer` on the end-user server are charged the plan's recall rate, exactly as the same calls on the Memory API are: a tenth of a credit on Free, nothing on Flex and Enterprise. No plan caps the number of calls. On Free a recall stops when the credits are spent, exactly as on the Memory API. - **Not in the Claude directory yet.** Add past.dev as a custom connector. It uses the same OAuth flow. # Troubleshooting > Check the address, the authentication method, the membership and the tool list. Product: past.dev MCP Server. Source: https://past.dev/docs/mcp/troubleshooting | What you see | What to do | | --- | --- | | **“Couldn't register” or the connector will not add** | Check the URL is exactly `https://app.past.dev/mcp` for the account server, with no trailing slash and no path after `/mcp`. Leave the advanced client ID and secret fields empty. | | **It connects, but the tool list is short** | The list is filtered by your platform role. A Member sees the project tools they reach and nothing about members or access; Billing sees the plan and usage and nothing else. Call `get_viewer` to see the role you are acting under. | | **It connects, but the tool list is empty** | The caller did not resolve to an accepted membership. Check that your invitation was accepted, and that the membership still exists. | | **A tool the docs list is missing** | Your role does not hold its scope, so it is never advertised. Section 03 names the role each tool needs. Ask an Owner or an Admin to change the role, or to run the action in the console. | | **A destructive call did nothing** | A destructive tool without `confirm: true` does nothing on purpose. It answers with the sentence describing what it would do. Read that sentence, then tell the assistant to go ahead. | | **`project-required`** | The organization has several live projects and the call named none. Give the project's slug, exactly as `get_viewer` or `list_projects` returned it. | | **`429` with `Retry-After`** | The per-person rate budget is spent. It is shared by every client and token you connected. Wait the stated interval. | | **Suddenly unauthorized mid-session** | The token expired and the refresh failed, or the membership changed. Reconnect the connector. On the end-user server, check that the identity still matches the connection's access rule. | | **One of your users is refused at sign-in** | The Refused page names the reason: the server is off, no login connection is enabled, the connection refuses people it has not seen, or the identity does not match the access rule. It never says which trait fell short. Check the rule on Build › MCP server. | | **Results are from the wrong organization** | The account connection follows your account's active organization. Switch it in the console; the change applies from the following call. | #### Confirming the server itself is healthy ``` curl -i -X POST https://app.past.dev/mcp \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize"}' ``` A healthy server returns `401` with a `WWW-Authenticate` header naming its discovery document. The client uses that response to find the sign-in service. A 403, a 404 or a timeout usually means the address is wrong. # Questions > Common questions about access, administration, cost and setup. Product: past.dev MCP Server. Source: https://past.dev/docs/mcp/faq #### Does connecting the account server give my assistant access to everything in past.dev? It gives the assistant exactly what you can reach in the console, and nothing else. Your platform role decides which tools appear, and your reach on a project decides which projects they work on. Permission checks run on every request and exclude other organizations. The account server runs no recall and writes no memory content. Its debugging tools read what Trace shows about the data points you sent, filtered by the same reach, so an assistant sees there exactly what you would see on the screen. #### Can it delete my data? It can do what you can do. `delete_project`, `revoke_api_key`, `delete_tag`, `delete_access_rule`, `delete_access_group`, `remove_member`, `set_member_role`, `merge_tags` and `revoke_project_grant` are destructive, and each one refuses to act until you have agreed to a sentence describing what it would do. Deleting memory is not one of them. That happens through the Memory API or in the console. #### Do I need an admin to set this up? For the account server, no. Any member can connect a client, and their connection reaches only what their role reaches. For the end-user server, yes. It is off until someone with project Admin enables it and connects an identity provider. #### Can my whole team share one connection? No. Each person connects with their own account. Every call is attributed to that person and bounded by their permissions, and every call is recorded with their name. #### Do my end users need past.dev accounts? No. They sign in at the identity provider you already run. past.dev is the authorization server and never a place for them to sign in, so they never see a past.dev account and past.dev never holds a password for them. #### What about an agent that cannot sign in? Mint an access link in the console. It is one address that is one identity, installed as the client's MCP server URL with nothing else to configure. The secret in the path is the credential, so mint one link per install and revoke the one you need to. #### How do I disconnect? Remove the connector in your AI client to stop normal use. For a lost device or a departing member, also remove the person's organization membership: that ends every account MCP session they hold, and the next call fails whatever the client still has. For one of your users, revoke their access link or change the trait their access depends on. #### Does it cost anything? Account tool calls are neither ingestions nor recalls, so they are not metered: reading your usage over MCP costs nothing. `recall` and `answer` on the end-user server are charged the plan's recall rate, exactly as the same calls on the Memory API are: a tenth of a credit on Free, nothing on Flex and Enterprise. No plan caps the number of calls. #### Does this work on a self-hosted deployment? Yes, and with no third party involved. The deployment is its own authorization server for the account server, and the end-user server runs on your own origin against your own identity provider. The same code runs in both places. #### Will it appear in the Claude directory? past.dev plans to join the directory. Until then, use the custom-connector setup in section 2. A directory listing would change discovery only, and would use the same OAuth flow and the same security controls. --- # past.dev Memory API # What the Memory API does > Send timestamped text, wait for it to complete, then recall ranked memory with the data points behind it. Product: past.dev Memory API. Source: https://past.dev/docs/memory-api/overview You send past.dev data points: pieces of text with the time each one happened, such as a call transcript, an email or a CRM note. past.dev derives memory from them. A recall names a reader and a question, and returns evidence: the documents that answer, each a memory with the excerpts of the data points behind it, or a data point itself. An audience decides which readers may see a data point. The API accepts text from conversations, email, meetings, support records, CRM notes and documents. Each data point keeps your source id and its occurrence time. Recall returns ranked documents with dates, artifact context and the exact excerpts of the data points behind each document. Five nouns organize every call. A project is the isolation boundary, and a project key selects one. A data point is one thing that happened, sent with its own time. An audience is the scope that decides which identities can read a data point. An identity is the reader a recall answers as. A trait is an attribute of an identity that audience rules match on. [Concepts](/docs/memory-api/concepts) defines each one. Read the [Quickstart](/docs/memory-api/quickstart) first, then [the Setup page](/docs/memory-api/setup) for the complete procedure in order. Read [Sending data](/docs/memory-api/sending-data) before a historical load and [Access control](/docs/memory-api/access-control) before the first customer goes live. All routes use JSON under `/api/v1`. The reference documents twenty-two operations in six groups, and the same contract is machine-readable at [/openapi.json](/openapi.json). - `POST /api/v1/ingest` accepts one data point, and `POST /api/v1/ingest/batch` accepts an ordered batch of up to 1,000. `GET /api/v1/ingest/{ingestionId}` reports completion and `DELETE` on the same route forgets the send. - `POST /api/v1/recall` returns ranked documents for one identity. - `DELETE /api/v1/data-points/{sourceId}` and `POST /api/v1/data-points/delete` delete data points by your own source id. - `/api/v1/audiences` creates, reads, replaces, deletes and recomputes audiences by slug. - `/api/v1/identities` creates, updates, bulk-replaces, lists, reads and deletes identity records and their traits. - `/api/v1/projects` lists and creates projects and mints or revokes project keys. These routes take the management key. **Get API key.** Sign up and start with 30,000 credits, 80,000 with a work email. No card required, and nobody approves the account. Create project keys in the console. Get API key: https://sso.past.dev/sign-up # Quickstart > Get a key, send a fact, send the fact that supersedes it, wait for the send to complete, then recall as one identity. Product: past.dev Memory API. Source: https://past.dev/docs/memory-api/quickstart ### Before the first call 1. Sign up at https://sso.past.dev/sign-up, or sign in at https://app.past.dev. Sign-up needs no approval and starts with 30,000 credits, 80,000 with a work email. A new organization has one project, `default-project`, with no data points. 2. Pick the project in the header. Every key, audience and data point belongs to the project shown there. 3. Open API keys, create a key, and store it in your secret store as `PAST_API_KEY`. The console can reveal it again. ### Four calls - **Base URL**: `https://api.past.dev/api/v1` - **Auth**: `Authorization: Bearer $PAST_API_KEY` **cURL** ```curl # 1. Send the fact at its original time. curl -X POST https://api.past.dev/api/v1/ingest \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "id": "call-8820", "content": "Nadia confirmed the Acme pilot ships March 14. Budget is 32k.", "label": "Account review call", "timestamp": "2026-02-03T09:00:00Z" }' # 2. Send the fact that supersedes it. curl -X POST https://api.past.dev/api/v1/ingest \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "id": "call-8821", "content": "Budget for the Acme pilot moved to 40k.", "label": "Email from Nadia", "timestamp": "2026-07-28T16:00:00Z" }' # 3. Poll the ingestionId from the last response until status is completed. curl https://api.past.dev/api/v1/ingest/7d9f2b6a-4c1e-4f8a-9b3d-2e5c8a1f6d40 \ -H "Authorization: Bearer $PAST_API_KEY" # 4. Recall as one identity. curl -X POST https://api.past.dev/api/v1/recall \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "What is the Acme pilot budget?", "identity": "demo-user", "level": "medium" }' ``` **Python** ```python import os import time import requests API = "https://api.past.dev/api/v1" HEADERS = { "Authorization": "Bearer " + os.environ["PAST_API_KEY"], "Content-Type": "application/json", } # 1. Send the fact at its original time. requests.post(API + "/ingest", headers=HEADERS, json={ "id": "call-8820", "content": "Nadia confirmed the Acme pilot ships March 14. Budget is 32k.", "label": "Account review call", "timestamp": "2026-02-03T09:00:00Z", }).raise_for_status() # 2. Send the fact that supersedes it. receipt = requests.post(API + "/ingest", headers=HEADERS, json={ "id": "call-8821", "content": "Budget for the Acme pilot moved to 40k.", "label": "Email from Nadia", "timestamp": "2026-07-28T16:00:00Z", }).json() # 3. Poll until this send is completed. while requests.get(API + "/ingest/" + receipt["ingestionId"], headers=HEADERS).json()["status"] != "completed": time.sleep(2) # 4. Recall as one identity. page = requests.post(API + "/recall", headers=HEADERS, json={ "query": "What is the Acme pilot budget?", "identity": "demo-user", "level": "medium", }).json() print(page["results"][0]["content"]) ``` **TypeScript** ```typescript const API = "https://api.past.dev/api/v1"; const headers = { Authorization: "Bearer " + process.env.PAST_API_KEY, "Content-Type": "application/json", }; const call = (path: string, init: RequestInit = {}) => fetch(API + path, { headers, ...init }).then((response) => response.json()); // 1. Send the fact at its original time. await call("/ingest", { method: "POST", body: JSON.stringify({ id: "call-8820", content: "Nadia confirmed the Acme pilot ships March 14. Budget is 32k.", label: "Account review call", timestamp: "2026-02-03T09:00:00Z", }) }); // 2. Send the fact that supersedes it. const receipt = await call("/ingest", { method: "POST", body: JSON.stringify({ id: "call-8821", content: "Budget for the Acme pilot moved to 40k.", label: "Email from Nadia", timestamp: "2026-07-28T16:00:00Z", }) }); // 3. Poll until this send is completed. while ((await call("/ingest/" + receipt.ingestionId)).status !== "completed") { await new Promise((resolve) => setTimeout(resolve, 2000)); } // 4. Recall as one identity. const page = await call("/recall", { method: "POST", body: JSON.stringify({ query: "What is the Acme pilot budget?", identity: "demo-user", level: "medium", }) }); console.log(page.results[0].content); ``` Poll `status` until it is `completed`. An accepted send is processed in the background, and its memory is readable once its status is `completed`. The `settled` flag in the same response describes every send in the project; [How it works](/docs/memory-api/how-it-works) defines both. The recall answers as `demo-user`. A data point sent without `audience` is visible to every identity of the project, so the identity can be any id. `audience` on a data point decides who reads it. See [Concepts](/docs/memory-api/concepts). ```200ok { "asOf": "2026-09-22T10:00:00Z", "usedEvidenceTokens": 18, "results": [ { "id": "39d9d745-c9f4-4884-a602-e538def76815", "rank": 1, "occurredAt": "2026-07-28T16:00:00Z", "content": "Acme pilot budget: currently 40k.", "artifact": { "id": "9f74f854-d11d-4bc4-bf68-5b2012ac9c15", "kind": "claim", "occurredAt": "2026-07-28T16:00:00Z" }, "sources": [ { "sourceId": "call-8821", "occurredAt": "2026-07-28T16:00:00Z", "metadata": { "channel": "sales" }, "excerpts": [ "Budget for the Acme pilot moved to 40k." ] } ] } ] } ``` ### Read the page Each result is one document with `id`, `rank`, `occurredAt`, and `content`. Its `artifact` carries the memory id, kind, occurrence date, and optional `supersededAt`. `artifact.kind` is `source` for an original data point or the kind of the derived memory. The sibling `sources` array groups this document’s supporting quotations by canonical `sourceId` and source date, with optional `metadata` and quotation strings in `excerpts`. These groups describe the supporting evidence for this returned document. An artifact can have additional provenance beyond the returned evidence. Original source documents keep their body in `content` without repeating it as a quotation. Pass `content` to your model in `rank` order and cite `sources[].sourceId`. An empty `results` array means nothing the identity may read matched the query. Both Quickstart data points stay in memory, each with its date. The sample shows the July value first. A read with `queryTimestamp` before July 28 sees the February value as the current one. # Set up an integration, step by step > Every call in order, with the response value that lets you move to the next step. Product: past.dev Memory API. Source: https://past.dev/docs/memory-api/setup This page is the whole procedure, from access to production. Each step names the call it makes and the response value that gates the next step. Every request is complete and runs against a fresh project. ### 1. Get access and keys The console at https://app.past.dev is the web app where your team creates keys, audiences and identities by hand, imports identities from a CSV, and sees every call on the Requests screen. Sign in, pick the project in the header, create a project key on the API keys screen and store it as `PAST_API_KEY`. A script that creates projects or mints keys for several customers also needs the management key, created under Settings, Organization, Management key by an Owner or Admin, and stored as `PAST_MANAGEMENT_KEY`. ### 2. Decide the scope Two rules hold everywhere: the audience is the scope of a data point, and the identity is the reader of a recall. Choose one project per customer when customers must never share memory, or one project with one audience per customer when they share one body of memory. [Access control](/docs/memory-api/access-control) compares the two and shows the CRM and transcript patterns. The steps below run inside one project. ### 3. Create the audiences Create every audience a data point will name before the first send. A `fixed` audience lists identity ids. A `rule` audience matches identities by their traits. The response returns `slug` and `kind`; `201` means created and `200` means replaced. ```curl curl -X PUT https://api.past.dev/api/v1/audiences/acme \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "kind": "rule", "name": "Acme", "rule": { "match": "all", "conditions": [ { "trait": "company", "operator": "is", "values": ["acme"] } ] } }' curl -X PUT https://api.past.dev/api/v1/audiences/deal-4410 \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "kind": "fixed", "name": "Acme renewal 2026", "identities": ["dana@example.com", "lee@example.com"] }' ``` ### 4. Create the identities and their traits Create the readers with `POST /api/v1/identities/bulk`, up to 1,000 per call, with the traits your rule audiences match on. The response lists each identity with `created` or `updated`, and a trait change recomputes the rule audiences it affects. An identity first named in a `fixed` audience already exists with `origin` `audience`; one created here carries `origin` `api`. ```curl curl -X POST https://api.past.dev/api/v1/identities/bulk \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '[ { "identity": "dana@example.com", "traits": { "company": "acme", "team": "sales" } }, { "identity": "lee@example.com", "traits": { "company": "acme", "team": "support" } } ]' ``` ### 5. Cut the history into data points One data point is one thing that happened: one conversation for one day, one email, one record, one document, timed at its first message. Give each one a stable `id` from your source system, an explicit `timestamp` with a zone, and the `audience` that may read it. Keep a conversation part under 16,000 characters and a document part under 32,000. [Sending data](/docs/memory-api/sending-data) gives the unit for each source type and the shape of the text. ### 6. Send the history Send with `POST /api/v1/ingest/batch`: up to 1,000 data points and 16 MiB of content per call, with one `idempotencyKey` per batch, a name you choose so that a retry sends nothing twice. Batches may run in parallel, since every data point carries its own time; oldest first is a recommendation that keeps the history complete up to a date while the load runs. Keep the `ingestionId` of every batch and the ids you sent; no route lists them. A `202` means accepted. A `400` refuses the whole batch and names the item in `debugMessage`. ```curl curl -X POST https://api.past.dev/api/v1/ingest/batch \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "idempotencyKey": "crm-backfill-2026-04-0001", "items": [ { "id": "crm-note-8841", "content": "Renewal call with Acme. Sam asked for per-seat billing and a quote for 40 seats before month end.", "label": "Renewal call", "timestamp": "2026-04-03T14:00:00Z", "audience": "deal-4410" }, { "id": "crm-company-acme-2026-04-07", "content": "Acme moved its headquarters to Lyon. Ada is the new procurement contact.", "label": "Company update", "timestamp": "2026-04-07T10:00:00Z", "audience": "acme" } ] }' ``` ### 7. Wait for each batch Poll `GET /api/v1/ingest/{ingestionId}` every 2 seconds until `status` is `completed`; the other values are `processing` and `failed`. The time to complete depends on the size of the send, and `status` is the signal. Polls spend no credits and are outside the per-minute limit. `failed` means the send stopped on an error: the answer carries `failure` with a `code` and a `message`, and `POST /api/v1/ingest/{ingestionId}/retry` runs the send again, free. If it stops again, write to support@past.dev with the `ingestionId` and the `X-Request-Id` header of the send. `settled`, `blocked` and the `readiness` counters describe every send in the project; [How it works](/docs/memory-api/how-it-works) defines them. ```curl curl https://api.past.dev/api/v1/ingest/7d9f2b6a-4c1e-4f8a-9b3d-2e5c8a1f6d40 \ -H "Authorization: Bearer $PAST_API_KEY" ``` ```200ok { "status": "completed", "settled": true, "blocked": false, "readiness": { "raw": 0, "comprehension": 0, "consolidation": 0, "failed": 0 } } ``` ### 8. Verify with a recall Recall as one identity of each scope and read the page. A correct setup returns a result whose `sources` carry a `sourceId` you sent. A recall as an identity outside the scope returns project-visible memory only; an empty page alone proves nothing, so confirm reach with `GET /api/v1/identities/{identity}`, which lists the audiences the identity reaches. ```curl curl -X POST https://api.past.dev/api/v1/recall \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "What did Acme ask for in the renewal?", "identity": "dana@example.com" }' # The same read as an identity outside both audiences returns project-visible memory only. curl -X POST https://api.past.dev/api/v1/recall \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "What did Acme ask for in the renewal?", "identity": "visitor@example.com" }' ``` ### 9. Go live - Send new data points as they happen, with the same id scheme and `audience` on every one. - Correct a record with the same `id` and the new text, and send `audience` again: a re-send without it makes the data point project-visible. - Retract with `DELETE /api/v1/data-points/{sourceId}` or `POST /api/v1/data-points/delete`; the data points and the memory derived from them leave recall at once. - Retry a `429` with no body after a pause. Retry a `5xx` with the same `idempotencyKey`. Fix the request on any other `4xx`. - Find any call by its `X-Request-Id` on the Requests screen of the console, and quote that id to support@past.dev. [The Limits table on Concepts](/docs/memory-api/concepts#limits) holds every number this page uses. # Authentication > Two kinds of bearer key open the API. Each route accepts one kind. Product: past.dev Memory API. Source: https://past.dev/docs/memory-api/authentication ```header Authorization: Bearer $PAST_API_KEY ``` | Key | Opens | | --- | --- | | **Project key** (past_sk_) | Ingest, completion, recall, data points, audiences and identities. The key selects one project, so `projectId` is never sent in a request body. Your application holds this key. | | **Management key** (past_mk_) | Projects and project keys, and the identity routes with `?project=`. It reads no memory. One exists per organization. | A `401` names the check that refused the key. No `Authorization` header, or `Bearer` with no value, returns `401 missing-api-key`. A header that does not read `Bearer `, or a bearer that is not a past key, returns `401 malformed-api-key`, and `debugMessage` says which. A well-formed key that is unknown, expired or revoked returns `401 unauthorized`, one answer for all three. A key of the kind a route does not accept returns `401 unauthorized` too, and `debugMessage` names the kind. On the identity routes, a management key without `?project=` returns `400 project-required`, and a project key with a `?project=` that names another project returns `400 project-mismatch`. ### Where keys are created Project keys are created on the API keys screen of the console at https://app.past.dev. A key belongs to the project shown in the header when it is created, and it never changes project. The console can reveal a key again. The API returns a key value once, in the response of `POST /api/v1/projects/{slug}/keys`. The management key is created under Settings, Organization, Management key, by an Owner or Admin, and rotated there; rotation replaces its value at once. For a fresh organization: sign in, keep `default-project` or create a project, create a project key, and start with the [Quickstart](/docs/memory-api/quickstart). Create the management key only when a script must create projects or keys. Key values and credentials are never accepted as fields in request bodies. Call the API from your server. Browser requests are refused by the CORS policy, whatever their origin, and a key in a browser is exposed to its users. A key for an archived project returns `403 project-archived` until the project is unarchived in the console. # Projects, data points, audiences, identities and traits > The five nouns, what each one is for, and every limit in one table. Product: past.dev Memory API. Source: https://past.dev/docs/memory-api/concepts A data point is what you send: one piece of text with the time it happened. A memory is a short statement past.dev derives from data points: a fact, a rule, a membership, a name or a dated event, kept with the excerpts that support it. A document is what a recall returns: a data point or a memory. Each result is one document with `artifact` context and `sources` evidence. Each source group carries your `sourceId`, its date, optional metadata, and verbatim quotation strings in `excerpts`. `artifact.id` identifies the underlying memory; several results can refer to the same artifact. A project holds all of them. An audience decides who can read a data point, an identity is the reader a recall answers as, and traits describe an identity. ### Project A project is the isolation boundary. A project key selects one project, so `projectId` is never sent in a request body. Data points, memory, audiences, identities and keys belong to exactly one project, and nothing crosses between projects. The console shows the current project in its header. Every project has one audience with the slug `project`. It reaches every identity of the project, and it is the scope of a data point sent without `audience`. Projects are created in the console or with `POST /api/v1/projects` and the management key. ### Data point A data point is one thing that happened, sent as raw text with the time it happened. It is the unit of replacement and of deletion. [Sending data](/docs/memory-api/sending-data) says how to cut a source into data points. | Field | What it is for | | --- | --- | | `id` (string) | Your stable id for this data point, unique in the project. Every response returns it as `sourceId`. A second send with the same id, content and audience changes nothing. Changed content replaces the data point and the memory derived from it. Supply it: when omitted, a hash of `content` is used, so a corrected text becomes a second data point. | | `content` (string, required) | The raw text, complete and unedited. Empty or whitespace-only content is refused. | | `timestamp` (ISO 8601) | When the thing happened. Every response returns it as `occurredAt`. When omitted, an existing id keeps its stored time and a new data point takes the send time. | | `label` (string) | A short title, stored with the data point. | | `metadata` (JSON) | Your own JSON, stored with the data point. Recall never filters on it. | | `audience` (string) | The slug of the audience that can read this data point. Omit it, or send `project`, for a data point every identity can read. | | `identity` (string) | Your id for the author of this data point. It creates the identity record on first sight and does not scope the data point. Set it only when the author is also a reader you want in the directory: every new value creates a record. The sample below sets it because Dana reads memory. | ```json { "id": "crm-note-8841", "content": "Renewal call with Acme. Sam asked for per-seat billing and a quote for 40 seats before month end.", "label": "Renewal call", "timestamp": "2026-04-03T14:00:00Z", "audience": "acme", "identity": "dana@example.com" } ``` ### Audience An audience is a named scope inside a project. Its slug, a short lowercase name used in URLs, is what a data point names in `audience`, and what a recall reaches through the identity. A slug has 2 to 40 characters: lowercase letters, digits and dashes, with a letter or digit at each end. Audiences are created on the Audiences screen of the console or with `PUT /api/v1/audiences/{slug}`. A data point that names a slug that does not exist is refused with `400 audience-unknown`. An audience you create has one of two kinds. A `fixed` audience lists the identity ids it reaches. A `rule` audience reaches every identity whose traits match its rule: `match` is `all` or `any`, over 1 to 20 conditions of the form trait, operator, values. The default audience `project` reports `kind` `default` on the wire. It reaches every identity, and it cannot be replaced or deleted. An audience whose list or rule names nobody holds memory that nobody can read until it names someone again. The memory of a deleted audience stays unreadable for good; the slug is free, and a new audience under that slug does not reach it. A change to a list, a rule or a trait applies to recall within a short window of seconds, and `POST /api/v1/audiences/{slug}/recompute` applies it now. A memory is never readable by more identities than the data points behind it: a reader must be able to read every data point a memory rests on. ```json { "kind": "rule", "name": "Acme", "rule": { "match": "all", "conditions": [ { "trait": "company", "operator": "is", "values": ["acme"] } ] } } ``` ### Identity An identity is your own id for a reader: a person, an agent or a service that calls recall. The id has 1 to 200 characters and is never generated by past.dev. Use a stable identifier your application already owns, such as an email address or a Slack user id. The same id is what every recall for that reader answers as. An identity record is created the first time the id is seen: in `identity` on a data point, in the list of a `fixed` audience, or through `PUT /api/v1/identities/{identity}`. A recall for an id that has no record reads project-visible memory only, and it never creates the record. `DELETE /api/v1/identities/{identity}` removes the record and its audience memberships. The memory stays. The Audiences screen of the console lists identities on its Identities tab. `origin` records how a record was created and never changes: `api` through the identity routes, `ingest` first seen on a data point, `audience` first named in a fixed list, `csv` a CSV import in the console, `console` created by hand in the console, `connector` a connected identity source. `lastSeenAt` is the time of the last data point or recall that carried the id, null until one does. ```json { "traits": { "company": "acme", "team": "sales" } } ``` ### Trait A trait is a string attribute of an identity: a key of 1 to 200 characters with one string value of up to 2,000 characters. Traits are written through the identity routes and the console, and audience rules match on them. `is`, `is_not`, `is_one_of`, `is_not_one_of` and `exists` match only an identity that has the trait; `does_not_exist` matches an identity without it, including one with no traits at all. - `is` and `is_not` take exactly one value. - `is_one_of` and `is_not_one_of` take 1 to 50 values. - `exists` and `does_not_exist` take no value. - `is_not` and `is_not_one_of` match only an identity that has the trait with another value. ### Metadata Metadata is your own JSON, stored with the data point. Recall has no metadata filter. Use an audience to decide who reads a data point, and use the identity to decide what a read reaches. When a re-send with the same `id` omits `metadata`, the stored value is kept. Six keys are reserved for the platform: `author`, `role`, `conversationId`, `sessionId`, `messageId` and `ordinal`. Every other key is stored as sent and has no meaning to the platform. ### What recall returns for an identity A recall names the identity it answers as. The read returns project-visible memory plus the memory of every audience the identity reaches, and nothing else. An identity reaches a `fixed` audience that lists it and every `rule` audience its traits match. `GET /api/v1/identities/{identity}` returns the slugs the identity reaches at that moment. A membership change reaches recall after a short window, so a read made in the seconds after the change can still use the previous membership. An id with no record reads project-visible memory only and raises no error, so validate the id on your side before the call. ### Limits | Limit | Value | | --- | --- | | Data points per batch | 1 to 1,000 | | Content per request | 16 MiB of UTF-8, single sends included | | Data point cut from a conversation | Under 16,000 characters per part; a longer source is split into parts, see Sending data | | Data point cut from a document | Under 32,000 characters per part | | Source id | Any non-blank string, trimmed; no length limit | | Identity id | 1 to 200 characters | | Trait key and value | Key 1 to 200 characters, value 1 to 2,000, one string per key | | Audience slug | 2 to 40 lowercase letters, digits and dashes, a letter or digit at each end | | Audience name | 1 to 200 characters | | Fixed audience list | No limit | | Rule | 1 to 20 conditions; 1 to 50 values for `is_one_of` and `is_not_one_of` | | Identities per bulk call | 1 to 1,000 | | Ids per delete call | 1 to 1,000 | | Recall `limit` | 1 to 200 documents, default 20. The page ends at `limit` or at the evidence budget, whichever comes first. | | Recall `level` and `maxTokens` | `low` 8,000 tokens by default, `medium` 16,000, `high` 24,000, `extra-high` 32,000, or a `maxTokens` figure; a token is about four characters. A budget above the plan's ceiling is lowered to it: 8,000 on Free and Flex, 32,000 on Enterprise. | | Identity list page | 1 to 1,000, default 100 | | Idempotency key | Up to 200 characters, no expiry | | Project name | Up to 255 characters, at least two letters or digits | | Key name | 1 to 64 characters | | Credits | One per 350 bytes of content, at least one per data point; prices on the pricing page | | Requests per minute | Per key: 500 on Free, 750 on Flex, 1,000 on Enterprise. No limit on a self-hosted deployment | | Projects per organization | 1 on Free, 5 on Flex, unlimited on Enterprise, archived projects included. No limit on a self-hosted deployment | | Request log payload retention | 7 days on Free, 90 days on Flex, unlimited on Enterprise. The record of the call stays after its payload expires | # How requests behave > Acceptance, completion, replacement, recall and deletion, as your application observes them. Product: past.dev Memory API. Source: https://past.dev/docs/memory-api/how-it-works 1. **Accept** A successful send returns an ingestion id after the request is durably accepted. 2. **Complete** Background processing continues after acceptance. Poll `GET /api/v1/ingest/{ingestionId}` until `status` is `completed`; the memory of that send is then readable. 3. **Recall** Recall returns up to the evidence budget `level` or `maxTokens` names: the memories that answer the query, each with artifact context and grouped source evidence. 4. **Forget** A deletion removes the data points and every memory that rests only on them, at once. A memory that also rests on other data points stays, readable by whoever can read those. ### Completion status A send has two `status` vocabularies. The receipt of `POST /api/v1/ingest` says `queued`, or `completed` when nothing changed. `GET /api/v1/ingest/{ingestionId}` says `processing`, `completed` or `failed` for that send. `settled`, `blocked` and `readiness` describe every send in the project. The four counters count sends: `raw` accepted and not started, `comprehension` in progress, `consolidation` reserved and always 0 today, `failed` stopped. `settled` is true when the first three are 0 and no send has failed. `blocked` is true when only failed sends remain. A failed send stopped on an error. It does not clear on its own, and its data points are kept. Its status carries `failure` with a `code` and a `message`. Run it again with `POST /api/v1/ingest/{ingestionId}/retry`, free. If it stops again, write to support@past.dev with the `ingestionId` and the `X-Request-Id` of the send. A re-send with the same content changes nothing; a re-send with changed content is processed as a new version. Before a send is `completed`, a recall can return its data points as `source` documents and none of the memory derived from them yet. Polls spend no credits and are outside the per-minute limit. ### Source replacement A caller-supplied `id` is stable within the project. A send with the same id, content and audience returns `200` with `unchanged: true`. A send with changed content replaces the data point and the memory that depended on the previous content. A send with the same content and another audience moves the data point to that audience and derives nothing new. On a re-send, every omitted field keeps its stored value except `audience`, which resets to project-visible; send `audience` on every re-send of a scoped data point. A deleted id is refused when sent again; use a new id. ### Idempotency keys `idempotencyKey` makes a send replay-safe across processes. The same key with the same body returns the original receipt, also while the first request is in flight, starts nothing new and spends no credits. The same key with a different body returns `409 idempotency-conflict`. Send an explicit `timestamp` on every data point of a keyed request: an omitted timestamp takes the send time, which makes the retry a different body. Keys belong to the project and never expire. ### Retries - `429` with no body: the per-minute limit. Wait one second, retry, and double the pause on each further `429`. There is no fixed limit on parallel sends beyond that rate. - `429 project-cap-reached`: the project's monthly cap. Stop sends until the month resets; recall continues to serve. - `402 out-of-credits`: the organization's credits are spent. Do not retry: waiting does not change the answer. `data.creditsLeft` and `data.creditsNeeded` say how far short the call fell, and `data.resetsOn` when the included credits come back, null on Pay as you go. Buy credits to continue. - `5xx`, a timeout or a dropped connection: retry the same request with the same `idempotencyKey`. `503 organization-not-ready` clears within minutes. `503 pipeline-disabled` is a deployment setting on self-host. - Any other `4xx`: fix the request. The same request again gives the same answer. ### Point in time `queryTimestamp` sets the perspective of a recall. Evidence that occurred at or before that instant is eligible, and later evidence is out of scope. It defaults to the time of the call. `occurredFrom` and `occurredTo` narrow the read to a window of occurrence time, both bounds inclusive. The window intersects with the perspective, so a window that lies after `queryTimestamp` matches nothing. A window whose start is after its end returns `400 recall-range-invalid`. ```curl curl -X POST https://api.past.dev/api/v1/recall \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "What is the Acme pilot budget?", "identity": "demo-user", "queryTimestamp": "2026-03-01T00:00:00Z" }' ``` This read sees the February value of the Quickstart as the current one. The July data point occurred after the perspective and is out of scope. ### Identity and audience A data point names the audience that can read it. A recall names the identity it answers as, and reaches that identity's audiences. [Concepts](/docs/memory-api/concepts) defines both, and [Access control](/docs/memory-api/access-control) shows how to place customers, companies and deals. ### Request ids Every `/api/v1` response, success or refusal, carries an `X-Request-Id` header that begins with `req_`. The console's Requests screen lists each call under that id. A client-supplied `X-Request-Id` is not echoed. ### Health `GET /health` is public on every deployment, cloud included, and returns `status`, `name` and `version`. A release image reports its release version; a build from source reports `dev`. ### Credits and limits On the cloud service, sends are metered in credits: one credit per 350 bytes of UTF-8 content, rounded up per data point, with at least one credit per data point, summed over a batch. Only the data points that start ingestion are charged: a new data point, or a new revision of one. An unchanged re-send, a re-send that only changes the `audience`, and a replay of an `idempotencyKey` cost nothing, and they are not refused at zero credits. Polls and deletion spend no credits, and a refused call costs nothing. A recall costs a tenth of a credit on Pay as you go, the one plan where reading is metered, and nothing on Flex and Enterprise. When the organization's credits are spent, a send returns `402 out-of-credits`. On Flex and Enterprise recall continues to serve. On Pay as you go a recall returns `402 out-of-credits` too, because that plan charges for it. A project can carry its own monthly caps on data points and credits, set under Settings, Projects in the console. A reached cap returns `429 project-cap-reached` until the month resets. No plan caps the number of recall calls: a recall is never refused for an allowance. Requests per minute follow the plan of the key's organization: 500 on Free, 750 on Flex and 1,000 on Enterprise, counted for each key over a fixed one-minute window on sends, recalls and answers. A request over the limit returns `429` with no body. A self-hosted deployment has no allowance. The plan also caps the evidence one recall returns and the number of projects. A recall returns up to 8,000 tokens of evidence on Free and Flex and up to 32,000 on Enterprise: a larger `maxTokens` is lowered to that ceiling before the read, and `usedEvidenceTokens` reports what the page carried. An organization holds 1 project on Free, 5 on Flex and any number on Enterprise, archived projects included, and a project past that count returns `402 project-limit-reached`. # Scoping memory to customers, companies and deals > Where a customer, a company or a deal lives, with the requests that put it there. Product: past.dev Memory API. Source: https://past.dev/docs/memory-api/access-control Two rules hold in every pattern on this page. The audience is the scope: a data point names the audience that can read it. The identity is the reader: a recall names the identity it answers as, and reaches that identity's audiences. ### Projects or audiences | Concern | Projects / Audiences | | --- | --- | | Isolation | Projects: nothing crosses between projects, and a code error in one integration cannot reach another project. Audiences: one body of memory, filtered per identity on every read. | | A leaked key | Projects: a project key reaches one project. Audiences: a project key reaches every audience of the project. | | Memory across customers | Projects: never connected. Audiences: the memory of one customer stays inside its audience, and a project-visible data point is read by every identity. | | Caps and usage | Projects: each project carries its own monthly caps and its own usage line in the console. Audiences: one set of caps and one usage line for all customers. | | Operations | Projects: one project and one key per customer, created with the management key. Audiences: one key, one audience per customer, and an identity per reader. | Use separate projects for separate authorization domains, and for separate environments such as staging and production. Use audiences for readers inside one body of memory. Nothing moves between projects: to give a customer its own project later, send its data points again with the new project's key. The plan sets the number of projects: 1 on Free, 5 on Flex and unlimited on Enterprise, archived projects included. Credits are counted for the organization across its projects, and each project can carry its own caps. ### One project per customer 1. Create the project with the management key. 2. Mint a project key for it and store the key with the customer's configuration. 3. Send the customer's data points with that key. Recall with the same key, as the customer's user. ```curl,managementkey curl -X POST https://api.past.dev/api/v1/projects \ -H "Authorization: Bearer $PAST_MANAGEMENT_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme" }' curl -X POST https://api.past.dev/api/v1/projects/acme/keys \ -H "Authorization: Bearer $PAST_MANAGEMENT_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme production" }' ``` The second response carries `key`, the clear value of the new project key. The API returns it in this response only; the console can reveal it again. The project's slug, `acme` here, is derived from its name and never changes. Every call below takes the project key of the customer's project. ### One project, one audience per customer Create a `fixed` audience per customer that lists the customer's users. Send every data point of that customer with the audience, on every send. Recall as one of the listed users. ```curl,projectkey curl -X PUT https://api.past.dev/api/v1/audiences/acme \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "kind": "fixed", "name": "Acme", "identities": ["sam@acme.example", "ada@acme.example"] }' curl -X POST https://api.past.dev/api/v1/ingest \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "id": "support-2026-04-02-ada", "content": "Ada asked whether the export runs on projects over 50,000 rows. Sam confirmed the fix ships on April 9.", "label": "Support conversation", "timestamp": "2026-04-02T09:15:00Z", "audience": "acme" }' curl -X POST https://api.past.dev/api/v1/recall \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "When does the export fix ship?", "identity": "ada@acme.example" }' ``` A user of another customer is absent from the `acme` list, so a recall as that user returns project-visible memory only. ### CRM memory by company and deal Identities are the users of your application. A trait `company` on each user names the company the user works for. A company is a `rule` audience on that trait, so a new user with the trait joins it with no change to the audience. A user works on several deals and a trait holds one string, so a deal is a `fixed` audience that lists the users on the deal. A data point about the company names the company audience. A data point about one deal names the deal audience. A recall as a user reaches both. ```curl,projectkey curl -X PUT https://api.past.dev/api/v1/identities/dana@example.com \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "traits": { "company": "acme", "team": "sales" } }' curl -X PUT https://api.past.dev/api/v1/audiences/acme \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "kind": "rule", "name": "Acme", "rule": { "match": "all", "conditions": [ { "trait": "company", "operator": "is", "values": ["acme"] } ] } }' curl -X PUT https://api.past.dev/api/v1/audiences/deal-4410 \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "kind": "fixed", "name": "Acme renewal 2026", "identities": ["dana@example.com", "lee@example.com"] }' curl -X POST https://api.past.dev/api/v1/ingest \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "id": "crm-note-8841", "content": "Renewal call with Acme. Sam asked for per-seat billing and a quote for 40 seats before month end.", "label": "Renewal call", "timestamp": "2026-04-03T14:00:00Z", "audience": "deal-4410" }' curl -X POST https://api.past.dev/api/v1/ingest \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "id": "crm-company-acme-2026-04-07", "content": "Acme moved its headquarters to Lyon. Ada is the new procurement contact.", "label": "Company update", "timestamp": "2026-04-07T10:00:00Z", "audience": "acme" }' curl -X POST https://api.past.dev/api/v1/recall \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "Who is the procurement contact at Acme and what did they ask for?", "identity": "dana@example.com" }' ``` ### Read one scope Recall has no filter on content or metadata: the query text, the reader and the time window decide the page. A read limited to one deal runs as a reader identity whose only audience is that deal. Create the reader, list it in the deal's audience, and send the deal's data points with that audience. The read then returns the deal's memory and the project-visible memory, so keep every scope's data points scoped, and put in the project-visible set only what every reader may see. ```curl,projectkey curl -X PUT https://api.past.dev/api/v1/audiences/deal-4410 \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "kind": "fixed", "name": "Acme renewal 2026", "identities": ["dana@example.com", "lee@example.com", "deal-4410-reader"] }' curl -X POST https://api.past.dev/api/v1/recall \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "What did Acme ask for in the renewal?", "identity": "deal-4410-reader" }' ``` ### A lead who changes company A lead is an identity when the lead reads memory, and a subject in the text when your users read about the lead. In both cases the id stays the same when the lead moves. Replace the `company` trait with `PATCH /api/v1/identities/{identity}`. The identity leaves the rule audience of the old company and enters the rule audience of the new one after the window. Earlier data points keep the audience they were sent with, so the history of the old company stays with the old company. ```curl,projectkey curl -X PATCH https://api.past.dev/api/v1/identities/person-2210 \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "traits": { "company": "globex" } }' ``` ### Call transcripts with attendees Send one data point per call, with `timestamp` at the start of the call. Put a header block at the top of the text: the title, the date and the attendees. Then the transcript, one line per speaker turn, with the speaker's name at the start of the line. Name the audience that can read the call: a `fixed` audience of the attendees' identities, or a `rule` audience on a team trait when the whole team reads every call. Recall as any attendee. A call longer than 16,000 characters is split into parts, as [Sending data](/docs/memory-api/sending-data) describes. ```curl,projectkey curl -X POST https://api.past.dev/api/v1/ingest \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "id": "call-9107", "content": "Call: Acme renewal review\nDate: 2026-05-12 14:00 UTC\nAttendees: Dana (Tamtam), Sam (Acme), Ada (Acme)\n\nDana: Let us start with the seat count.\nSam: We are at 38 seats today and expect 45 by June.\nAda: Finance wants one annual invoice.\nDana: Noted. I will send the quote for 45 seats by Friday.", "label": "Acme renewal review", "timestamp": "2026-05-12T14:00:00Z", "audience": "acme-account-team", "identity": "dana@example.com" }' ``` ### A fact that changes Two ways exist to change a fact. A send with the same `id` and new content replaces the data point: the old text and the memory derived from it are gone. A send with a new `id` adds a dated fact, and a recall returns both data points, each with its own date. Use the same id to correct a record. Use a new id for a new event. ```curl,projectkey # A correction: the same id replaces the record. curl -X POST https://api.past.dev/api/v1/ingest \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "id": "crm-note-8841", "content": "Renewal call with Acme. Sam asked for per-seat billing and a quote for 45 seats before month end.", "label": "Renewal call", "timestamp": "2026-04-03T14:00:00Z", "audience": "deal-4410" }' # A new event: a new id adds a dated fact next to the earlier one. curl -X POST https://api.past.dev/api/v1/ingest \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "id": "email-4f1c9a", "content": "Sam confirmed 45 seats and asked to move the renewal date to October 1.", "label": "Email from Sam", "timestamp": "2026-04-10T08:30:00Z", "audience": "deal-4410" }' ``` ### Four rules - Send `audience` on the data point and `identity` on the read. The audience is the scope. The identity is the reader. - Use a reader identity for a scoped read. Metadata never filters a recall. - Create the audience before a data point names it. An unknown slug is refused with `400 audience-unknown`. - Send `audience` on every re-send of a scoped data point. A re-send without it makes the data point project-visible. # What to send and how to split it > The unit recall reads best, the size of one data point, the shape of its text, and the procedure for a historical load. Product: past.dev Memory API. Source: https://past.dev/docs/memory-api/sending-data The unit that recall reads best is one data point per conversation and day, per email, per record, per document. Every message keeps its own time and speaker inside the text, and the data point is timed at its first message. A source is never sent as one block, and never as one line per message. ### The unit | Source | One data point per | | --- | --- | | Chat log or thread | One channel and one day. Every message keeps its own time and author in the text. Timed at the first message of the day. | | Email | One message, with its subject, sender, recipients and date at the top. Timed at the `Date` header. | | Call or meeting transcript | One call, with a header block and one line per speaker turn. Timed at the start of the call. | | CSV or table | One row. Timed at the row's date column. | | JSON records | One object. Timed at the object's date key. | | Document | One document, with its title and date at the top. Timed at the document date. | ### Size Keep a conversation part under 16,000 characters and a document part under 32,000 characters. Split a longer conversation at a message boundary and a longer document at a paragraph boundary, so that each part is one thing with its own dates. Give each part the same header line, an `id` of the form `.`, and a label that ends in the part number. A data point per message loses the conversation around each message, and a month of chat in one data point loses the date of each day. ### What the text looks like Send the text raw and complete. Start with one header line that names the source: the channel and the day, the subject of the email, the title of the document. Keep the speaker and the time on each message. Do not summarize, and do not strip names. ```good #support, 2026-04-02 [09:15] Ada (Acme): Does the export run on projects over 50,000 rows? [09:21] Sam (Tamtam): It fails today on the migration step. The fix ships on April 9. [09:22] Ada (Acme): Thanks. Finance needs the quarterly export on April 12. ``` ```avoid fix ships april 9 ``` ### Ids Give every data point a stable `id` derived from your source system. Every response returns it as `sourceId`. A second send with the same id, content and audience changes nothing. A send with the same id and new content replaces the data point. When `id` is omitted, the lowercase SHA-256 of the content is used, so a corrected text becomes a second data point next to the first. Supply your own id so that a correction replaces the original. An id appears in a path on `DELETE /api/v1/data-points/{sourceId}`. Percent-encode it there, and avoid `/` in ids: a slash cannot travel in a path segment. `POST /api/v1/data-points/delete` takes any id in its body. - `meeting-` for a call, and `meeting-.2` for its second part. - `email-` for an email. - `crm-note-` for a CRM record. - `slack--` for one channel and one day. ### Timestamps Send `timestamp` as ISO 8601 with a zone, and prefer UTC. A local time without a zone is ambiguous and can land on the wrong day. Backfilled history keeps its original date, so last year's records are recalled as last year's records. When `timestamp` is omitted, a new data point takes the send time and an existing id keeps its stored time. The wire field is `timestamp`. Every response returns it as `occurredAt`. ### Historical load 1. Create the audiences the data points will name, with `PUT /api/v1/audiences/{slug}` or on the Audiences screen. 2. Create the identities and their traits with `POST /api/v1/identities/bulk`, up to 1,000 per call. 3. Cut the source into data points as this page describes, and order them oldest first. 4. Send them with `POST /api/v1/ingest/batch`, up to 1,000 data points and 16 MiB of content per call, with an explicit `timestamp` on every item and one `idempotencyKey` per batch, so that a retry sends nothing twice. 5. Poll `GET /api/v1/ingest/{ingestionId}` of each batch every 2 seconds until `status` is `completed`. Polls spend no credits and are outside the per-minute limit. 6. Recall as one identity of each scope and check that the results are the expected ones. 7. Switch to live sends with the same ids and the same audiences, so that a later correction replaces its original. Batches may run in parallel. Every data point carries its own time, so the order of arrival does not change the result, and oldest first keeps the history complete up to a date while the load runs. Keep your own record of the ids and the ingestion ids you sent; no route lists them. Requests per minute follow the plan: 500 on Free, 750 on Flex and 1,000 on Enterprise, for each key. A send over the limit returns `429` with no body, so pace the batches and retry after a pause. A project cap that is reached returns `429 project-cap-reached` until the month resets. Credits are one per 350 bytes of content, at least one per data point, and a refused call costs nothing. ### Corrections and deletion To correct a data point, send it again with the same `id`, the new text and its `audience`. To delete data points, name their ids: `DELETE /api/v1/data-points/{sourceId}` for one, or `POST /api/v1/data-points/delete` for up to 1,000. Every id must name a data point that is accepted and not deleted, or nothing is deleted. To forget a whole send, call `DELETE /api/v1/ingest/{ingestionId}`. The data points leave recall at once, and storage cleanup finishes in the background. A deleted id cannot be sent again; use a new id. Deletion spends no credits. ```curl curl -X POST https://api.past.dev/api/v1/data-points/delete \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "ids": ["crm-note-8841", "email-4f1c9a"] }' ``` # Endpoint reference > Thirty-three operations under /api/v1. Field names match the backend JSON contract. Product: past.dev Memory API. Source: https://past.dev/docs/memory-api/api-reference ### POST /api/v1/ingest Accept one data point. Project key. Returns `202` with `status` `queued` when processing is queued, or `200` with `status` `completed` and `unchanged: true` when the id, the content and the audience are all unchanged. | Field | Description | | --- | --- | | `content` (string, required) | Raw text. Empty or whitespace-only content is refused. | | `label` (string) | A short title, stored with the data point. | | `id` (string) | Stable source id, returned as `sourceId`. When omitted, the lowercase SHA-256 of `content` is used. | | `timestamp` (ISO 8601) | When the content occurred, returned as `occurredAt`. Defaults to the stored value for an existing id, otherwise now. Stored at microsecond precision. Send it explicitly with an `idempotencyKey`. | | `metadata` (JSON) | Your own JSON, stored with the data point. Omission retains the stored value for an existing id. The keys `author`, `role`, `conversationId`, `sessionId`, `messageId` and `ordinal` are reserved for the platform. Recall never filters on metadata. | | `audience` (string) | Audience slug. Omit it or send `project` for project-visible memory. A re-send without it makes an existing data point project-visible. | | `identity` (string) | Customer id for the author, up to 200 characters. Its first appearance creates the directory record. It does not scope the data point. | | `idempotencyKey` (string) | Replay key for this request, up to 200 characters. | ```curl curl -X POST https://api.past.dev/api/v1/ingest \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "id": "call-8821", "content": "Budget for the Acme pilot moved to 40k.", "label": "Email from Nadia", "timestamp": "2026-07-28T16:00:00Z", "audience": "sales" }' ``` ```202accepted { "projectId": 12, "ingestionId": "7d9f2b6a-4c1e-4f8a-9b3d-2e5c8a1f6d40", "sourceId": "call-8821", "status": "queued", "unchanged": null } ``` ```200ok,unchanged { "projectId": 12, "ingestionId": "7d9f2b6a-4c1e-4f8a-9b3d-2e5c8a1f6d40", "sourceId": "call-8821", "status": "completed", "unchanged": true } ``` `projectId` is the numeric id of the key's project. An unknown audience slug returns `400 audience-unknown`. A send over 16 MiB of content returns `413 content-too-large`. Spent credits return `402 out-of-credits`, and a reached project cap returns `429 project-cap-reached`. ### POST /api/v1/ingest/batch Accept one atomic batch. Project key. A batch contains 1 to 1,000 items and at most 16 MiB of UTF-8 content, and every item is accepted or the batch is refused whole. A refusal names the item by its position in `debugMessage`. | Field | Description | | --- | --- | | `items` (array, required) | Ordered data points. Each item accepts every single-ingest field except `idempotencyKey`. A source id may appear once per batch. | | `idempotencyKey` (string) | Replay key for the complete batch, up to 200 characters. | ```curl curl -X POST https://api.past.dev/api/v1/ingest/batch \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "idempotencyKey": "backfill-2026-07-batch-0001", "items": [ { "id": "call-8821", "content": "Budget for the Acme pilot moved to 40k.", "timestamp": "2026-07-28T16:00:00Z", "audience": "sales" }, { "id": "call-8822", "content": "Nadia asked for the revised scope by August 4.", "timestamp": "2026-07-29T09:30:00Z", "audience": "sales" } ] }' ``` ```202accepted { "projectId": 12, "ingestionId": "a144be5e-7d82-4c79-b85a-90856865418d", "status": "queued", "unchanged": null, "items": [ { "ordinal": 0, "sourceId": "call-8821", "status": "queued", "unchanged": null }, { "ordinal": 1, "sourceId": "call-8822", "status": "queued", "unchanged": null } ] } ``` The top-level `status` and `unchanged` describe the batch: `queued` when any item is queued, `completed` and `true` when every item is unchanged. Each item carries its own pair, and `ordinal` is the item's position in the batch, which carries no other meaning. An empty batch, a repeated source id or more than 1,000 items returns `400 invalid-ingestion`. More than 16 MiB of content returns `413 invalid-ingestion`. ### POST /api/v1/ingest/estimate Cut files into data points, batch them and price them, before anything is sent. Project key. Each file is read for what it is, and cut where that kind breaks: one message per email, one day per chat, one section per document, one row per table. The answer names the kind each file was read as and the rule that decided it. It writes nothing, spends nothing, and leaves no row in the request log. | Field | Description | | --- | --- | | `files` (array) | The files to cut: `path`, the path inside the import, whose extension helps detection, `content` as text, and an optional `kind` that skips detection. At most 10,000 files. Extract PDF or office text yourself first. Send `files` or `items`, never both and never neither. | | `items` (array) | Data points you cut yourself, in the shape `POST /api/v1/ingest/batch` takes. They are batched and priced as given, with no detection. | | `timeZone` (string) | IANA zone reading the times a file writes without one, for example `Europe/Paris`. UTC when omitted. | | `points` (string) | `summary`, the default, lists each point with a 160-character preview, up to 1,000 rows; `none` lists only the files and the batches; `full` adds each point's content and metadata, ready to send. | A `kind` is `email`, `chat`, `transcript`, `table`, `records`, `code`, `documentation` or `notes`. Each batch is at most 500 data points and 8 MiB of content, half of what the batch route takes, and carries the `idempotencyKey` to send it with. `fits` is false when the import costs more than what is left of the credits or of the project's cap, and `fitsFirst` is how many points, in time order, do fit. `metered` is false on a deployment with no plan, and the credit figures are then null. A file that is empty, unreadable, over the size limit, or unparseable as the kind you named is listed with `skipped` and priced at zero. ```curl curl -X POST https://api.past.dev/api/v1/ingest/estimate \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "files": [{ "path": "support/acme.md", "content": "# Onboarding\n\nAcme runs the pilot in Q4." }], "timeZone": "Europe/Paris" }' ``` ```200ok { "files": [ { "path": "support/acme.md", "kind": "documentation", "detectedBy": "extension .md", "points": 1, "bytes": 42, "credits": 1, "skipped": null } ], "dataPoints": 1, "bytes": 42, "credits": 1, "batches": [ { "index": 0, "from": 0, "to": 0, "points": 1, "bytes": 42, "credits": 1, "idempotencyKey": "import-9f26c1-0" } ], "points": [ { "id": "file:support/acme.md#1", "kind": "documentation", "path": "support/acme.md", "label": "Onboarding", "timestamp": null, "identity": null, "audience": null, "bytes": 42, "credits": 1, "preview": "Onboarding. Acme runs the pilot in Q4." } ], "pointsOmitted": 0, "balance": { "left": 27340, "resetsAt": "2026-10-01T00:00:00Z", "projectCapLeft": null, "ingestionCapReached": false }, "fits": true, "fitsFirst": 1, "metered": true, "timeZone": "Europe/Paris" } ``` ### GET /api/v1/ingest/{ingestionId} Return the state of one send and the readiness of the whole project. Project key. `status` describes this send: `processing`, `completed`, or `failed`. Poll until `status` is `completed`. `settled`, `blocked` and `readiness` describe every send in the project, and the four counters count sends: `raw` accepted and not started, `comprehension` in progress, `consolidation` reserved and always 0 today, `failed` stopped. `settled` is true when the first three are 0 and no send has failed; `blocked` is true when only failed sends remain. An unknown ingestion id returns `404 not-found`. Polls spend no credits and are outside the per-minute limit. A `failed` send carries `failure` with a `code` and a `message`; every other status carries `failure: null`. Run it again with `POST /api/v1/ingest/{ingestionId}/retry`. ```curl curl https://api.past.dev/api/v1/ingest/7d9f2b6a-4c1e-4f8a-9b3d-2e5c8a1f6d40 \ -H "Authorization: Bearer $PAST_API_KEY" ``` ```200ok { "status": "processing", "settled": false, "blocked": false, "readiness": { "raw": 3, "comprehension": 1, "consolidation": 0, "failed": 0 } } ``` - `model-unavailable`: a model call failed on every attempt, because the provider was down, rate-limited or timed out. A retry usually gets it through. - `model-response-unreadable`: the model answered, but not in the shape the platform could read. A retry asks it again. - `index-unavailable`: the search index could not be written on any attempt. A retry usually gets it through. - `content-rejected`: a step refused the content itself, because the model provider answered 400, 403 or 413, or the text held nothing to index. A retry stops the same way, so change the content and send it again. - `internal`: anything else, and a send that stopped before reasons were recorded. ### POST /api/v1/ingest/{ingestionId}/retry Run a send that stopped again, under the same ingestion id. Project key. The send rejoins the project's queue with the revisions it carried, and its status then moves like any other send's. A retry is free: the send already paid, and the platform stopping is not your cost, so it spends no credits and is checked against no cap. Returns `202` with `ingestionId` and `status` `queued`. A send that has not stopped, whether it is still running or already complete, returns `409 ingestion-not-stopped` and is left as it was. An unknown ingestion id returns `404 not-found`, and a deployment with background ingestion disabled returns `503 pipeline-disabled`. Retry is the way back, because sending the same content again does nothing: identical content is unchanged, the same idempotency key returns the stopped send's own receipt, and a deleted id is refused. A `failure.code` of `content-rejected` is the one reason a retry does not fix. Change the content and send it as a new revision. ```curl curl -X POST https://api.past.dev/api/v1/ingest/7d9f2b6a-4c1e-4f8a-9b3d-2e5c8a1f6d40/retry \ -H "Authorization: Bearer $PAST_API_KEY" ``` ```202accepted { "ingestionId": "7d9f2b6a-4c1e-4f8a-9b3d-2e5c8a1f6d40", "status": "queued" } ``` ### GET /api/v1/ingest/{ingestionId}/explain Say where one send got to and what to do next, in one call. Project key. This is the first read to make when data does not show up: it joins what the request log, the send and the memory drawn from it know. `phase` is `queued`, `processing`, `captured`, `indexed`, `failed` or `nothing-changed`. `failure` is present only when the phase is `failed`, and its `retryable` is false only for `content-rejected`. Each item says what the send did with one data point, whether its text is searchable yet, how many memories came out of it, and the `sourceId` that the lineage read takes as a root. The first 100 items are listed and `itemsOmitted` counts the rest. `next` is one sentence naming the step to take. A request id may be used instead of the ingestion id through the account MCP server's `explain_ingestion`, which answers the same shape. A call refused before a send existed answers its status and error code with no send fields. An unknown ingestion id returns `404 not-found`. The read leaves no row in the request log and spends no credits. ```curl curl https://api.past.dev/api/v1/ingest/7d9f2b6a-4c1e-4f8a-9b3d-2e5c8a1f6d40/explain \ -H "Authorization: Bearer $PAST_API_KEY" ``` ```200ok { "ingestionId": "7d9f2b6a-4c1e-4f8a-9b3d-2e5c8a1f6d40", "requestId": "req_7f3a9c21", "status": 202, "errorCode": null, "acceptedAt": "2026-09-15T10:00:00Z", "settledAt": "2026-09-15T10:00:42Z", "phase": "indexed", "failure": null, "items": [ { "dataPointId": "call-8821", "revision": 1, "status": "created", "searchable": true, "memories": 3, "sourceId": "b1c2d3e4-5f60-4a7b-8c9d-0e1f2a3b4c5d" } ], "itemsOmitted": 0, "credits": 12, "next": "Nothing to fix: the content is searchable and its memories are ready. Read a source's lineage to see them." } ``` ### DELETE /api/v1/ingest/{ingestionId} Forget every data point accepted in the send and erase memory that depended on them. Project key. The data points leave recall at once, and storage cleanup finishes in the background. Returns `204` with no body. A send whose data points are already deleted returns `204` and changes nothing. An unknown ingestion id returns `404 not-found`. ```curl curl -X DELETE https://api.past.dev/api/v1/ingest/7d9f2b6a-4c1e-4f8a-9b3d-2e5c8a1f6d40 \ -H "Authorization: Bearer $PAST_API_KEY" ``` ### DELETE /api/v1/data-points/{sourceId} Delete one data point by your own source id, and the memory derived from it. Project key. Percent-encode the id in the path; an id that contains `/` is deleted through `POST /api/v1/data-points/delete`. The data point leaves recall at once. Returns `202` with `operationId`, the id of the storage cleanup, for support. An id that names no data point, or a deleted one, returns `404 data-point-not-found`. ```curl curl -X DELETE https://api.past.dev/api/v1/data-points/call-8821 \ -H "Authorization: Bearer $PAST_API_KEY" ``` ```202accepted { "operationId": "c2f6d5a0-3b1e-4d7a-9c48-5e2a7b1f9d03", "ids": ["call-8821"] } ``` ### POST /api/v1/data-points/delete Delete up to 1,000 data points by source id in one call. Project key. Every id must name a data point that is accepted and not deleted, or nothing is deleted and the response is `404 data-point-not-found` with the ids that are not. An unsettled data point counts as accepted. Repeated ids count once. The data points leave recall at once. Returns `202` with the cleanup operation and the ids it covers. | Field | Description | | --- | --- | | `ids` (array, required) | 1 to 1,000 source ids. An empty list or more than 1,000 ids returns `400 invalid-deletion`. | ```curl curl -X POST https://api.past.dev/api/v1/data-points/delete \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "ids": ["call-8821", "call-8822"] }' ``` ```202accepted { "operationId": "c2f6d5a0-3b1e-4d7a-9c48-5e2a7b1f9d03", "ids": ["call-8821", "call-8822"] } ``` ### POST /api/v1/recall Return ranked JSON documents with no generated answer. Project key. The read answers as `identity` and reaches project-visible memory plus that identity's audiences. The page carries up to the evidence budget `level` or `maxTokens` names. | Field | Description | | --- | --- | | `query` (string, required) | Natural-language query. | | `limit` (integer) | Maximum number of documents in the page. Defaults to 20 and accepts 1 to 200. | | `level` (string) | The evidence budget in words, so you can size the prompt you feed: `low` (the default, 8,000 tokens), `medium` (16,000), `high` (24,000) or `extra-high` (32,000). A level above the plan's ceiling is lowered to it: 8,000 on Free and Flex, 32,000 on Enterprise. | | `maxTokens` (integer) | An explicit token budget for document content and excerpt text, excluding JSON structure and metadata, for a caller who needs a figure the levels do not offer. Lowered to the plan's ceiling in the same way. Send `level` or `maxTokens`, never both. Whole documents are kept or omitted. | | `queryTimestamp` (ISO 8601) | The perspective of the read. Evidence that occurred at or before this instant is eligible. Defaults to now. | | `occurredFrom` (ISO 8601) | Inclusive start of the occurrence window. | | `occurredTo` (ISO 8601) | Inclusive end of the occurrence window. A start after the end returns `400 recall-range-invalid`. | | `sort` (string) | `relevance` by default, or `chronological`: the selected documents ordered oldest first, each with its relevance `rank` kept. | | `identity` (string, required) | Identity the read answers as, up to 200 characters. | ```curl curl -X POST https://api.past.dev/api/v1/recall \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "What is the Acme pilot budget?", "identity": "demo-user", "limit": 20, "maxTokens": 8000, "occurredFrom": "2026-01-01T00:00:00Z", "sort": "relevance" }' ``` ```200ok { "asOf": "2026-09-22T10:00:00Z", "usedEvidenceTokens": 18, "results": [ { "id": "39d9d745-c9f4-4884-a602-e538def76815", "rank": 1, "occurredAt": "2026-07-28T16:00:00Z", "content": "Acme pilot budget: currently 40k.", "artifact": { "id": "9f74f854-d11d-4bc4-bf68-5b2012ac9c15", "kind": "claim", "occurredAt": "2026-07-28T16:00:00Z" }, "sources": [ { "sourceId": "call-8821", "occurredAt": "2026-07-28T16:00:00Z", "metadata": { "channel": "sales" }, "excerpts": [ "Budget for the Acme pilot moved to 40k." ] } ] } ] } ``` - `asOf` is the UTC perspective used by recall. - `results` contains one entry per document, ordered by relevance unless chronological sorting was requested. Each entry has sibling `artifact` context and `sources` evidence groups. A replaced data point produces new ids. An empty array means nothing the identity may read matched. - `artifact.kind`, on each result, is `source` for a data point you sent, returned verbatim, or the kind of a memory derived from data points: `observation` (something a source stated), `claim` (a subject, a property and its value), `rule` (a standing instruction or policy), `membership` (a member of a set), `identity` (one name for another), `mention` (a topic raised, asked, requested or decided), `temporal-anchor` (an event and its date). The list can grow; treat an unknown value as a memory. - `rank` is numbered across the whole page after budget selection, so ranks run from 1 without gaps. - `rank` orders documents by relevance to the query. The application evaluates whether their content supports an answer. `artifact.supersededAt` is present when a later statement readable by the viewer replaced this one. - `sources` groups supporting evidence by canonical `sourceId` and source `occurredAt`, with optional `metadata` and quotation strings in `excerpts`. The id is the one you sent. Source documents carry their attribution in a source group and their body in `content`; they do not duplicate the body in `excerpts`. - `usedEvidenceTokens` counts document content and excerpt text at roughly four characters per token, rounded up per document. It excludes JSON structure, IDs, dates and metadata. It stays within the evidence budget; the complete response and model prompt can be larger. - Whole documents are admitted under the budget `level` or `maxTokens` names; document content and excerpts are never shortened to fit, and a document that does not fit is omitted. - A missing `query` returns `400 recall-query-required`, a missing `identity` returns `400 identity-required`, and an unknown `sort` returns `400 recall-sort-invalid`. ### PUT /api/v1/identities/{identity} Create or replace one identity and its complete trait set. Project key, or a management key with `?project=`. The identity id is 1 to 200 characters, percent-encoded in the path. Returns `200` with the record. | Field | Description | | --- | --- | | `traits` (object) | String values keyed by trait name. A key is 1 to 200 characters and a value 1 to 2,000. Omitted or empty replaces the set with no traits. A value that is neither a string nor null returns `400 traits-invalid`. | ```curl curl -X PUT https://api.past.dev/api/v1/identities/dana@example.com \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "traits": { "company": "acme", "team": "sales" } }' ``` ```200ok { "identity": "dana@example.com", "traits": { "company": "acme", "team": "sales" }, "origin": "api", "lastSeenAt": null, "createdAt": "2026-09-15T10:00:00Z", "updatedAt": "2026-09-15T10:00:00Z" } ``` ### PATCH /api/v1/identities/{identity} Merge traits into one identity. Project key, or a management key with `?project=`. Missing keys remain unchanged; an explicit null removes that key. Returns `200` with the record. | Field | Description | | --- | --- | | `traits` (object) | String or null values keyed by trait name. | ```curl curl -X PATCH https://api.past.dev/api/v1/identities/dana@example.com \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "traits": { "team": null, "region": "emea" } }' ``` ### POST /api/v1/identities/bulk Replace traits for up to 1,000 identities in one transaction. Project key, or a management key with `?project=`. The body is a non-empty JSON array of `{ identity, traits }` rows. A malformed row refuses the complete batch with `400 bulk-invalid-rows`; repeated identities use the last row. A concurrent write to the same identities returns `409 conflict` and nothing is applied; retry the batch. ```curl curl -X POST https://api.past.dev/api/v1/identities/bulk \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '[ { "identity": "dana@example.com", "traits": { "company": "acme", "team": "sales" } }, { "identity": "lee@example.com", "traits": { "company": "acme", "team": "support" } } ]' ``` ```200ok { "identities": [ { "identity": "dana@example.com", "status": "updated" }, { "identity": "lee@example.com", "status": "created" } ] } ``` ### GET /api/v1/identities List identities newest first. Project key, or a management key with `?project=`. The response contains `identities`, `hasMore`, and `endCursor`. - `after`: the `endCursor` of the previous page. - `limit`: page size, 1 to 1,000, default 100. A value outside the range is clamped. - `traitKey`: alone, lists the identities that have this trait. - `traitValue`: with `traitKey`, lists the identities whose trait has this exact value. - `project`: the project slug, with a management key. ```curl curl "https://api.past.dev/api/v1/identities?traitKey=company&traitValue=acme&limit=100" \ -H "Authorization: Bearer $PAST_API_KEY" ``` ```200ok { "identities": [ { "identity": "dana@example.com", "traits": { "company": "acme", "team": "sales" }, "origin": "api", "lastSeenAt": "2026-09-15T09:58:00Z", "createdAt": "2026-09-15T10:00:00Z", "updatedAt": "2026-09-15T10:00:00Z" } ], "hasMore": false, "endCursor": null } ``` ### GET /api/v1/identities/{identity} Return one identity with its traits, origin, timestamps, and the audience slugs it currently reaches. Project key, or a management key with `?project=`. `origin` is `api`, `ingest`, `csv`, `audience`, `connector` or `console`, set at creation. `lastSeenAt` is the time of the last data point or recall that carried the id. This is the one read that carries `audiences`. An unknown identity returns `404 not-found`. ```curl curl https://api.past.dev/api/v1/identities/dana@example.com \ -H "Authorization: Bearer $PAST_API_KEY" ``` ```200ok { "identity": "dana@example.com", "traits": { "company": "acme", "team": "sales" }, "origin": "api", "lastSeenAt": "2026-09-15T09:58:00Z", "createdAt": "2026-09-15T10:00:00Z", "updatedAt": "2026-09-15T10:00:00Z", "audiences": ["project", "acme", "deal-4410"] } ``` ### DELETE /api/v1/identities/{identity} Delete the directory record and remove it from audiences. Project key, or a management key with `?project=`. Stored memory is not deleted. Returns `204` with no body. An unknown identity returns `404 not-found`. ```curl curl -X DELETE https://api.past.dev/api/v1/identities/dana@example.com \ -H "Authorization: Bearer $PAST_API_KEY" ``` ### GET /api/v1/audiences List the audiences of the project. Project key. The default audience `project`, with `kind` `default`, comes first, then the named audiences by creation time. ```curl curl https://api.past.dev/api/v1/audiences \ -H "Authorization: Bearer $PAST_API_KEY" ``` ```200ok { "audiences": [ { "slug": "project", "name": "Project", "kind": "default", "memberCount": 14, "memoryCount": 320, "reachesNobody": false, "recomputedAt": null, "createdAt": "2026-08-01T00:00:00Z", "identities": null, "rule": null }, { "slug": "acme", "name": "Acme", "kind": "rule", "memberCount": 3, "memoryCount": 41, "reachesNobody": false, "recomputedAt": "2026-09-15T10:00:00Z", "createdAt": "2026-09-15T10:00:00Z", "identities": null, "rule": { "match": "all", "conditions": [ { "trait": "company", "operator": "is", "values": ["acme"] } ] } } ] } ``` ### PUT /api/v1/audiences/{slug} Create or replace one audience by slug. Project key. Returns `201` with the record on create and `200` on replace. The slug has 2 to 40 characters, lowercase letters, digits and dashes, with a letter or digit at each end; anything else returns `400 audience-slug-invalid`. The slug `project` returns `400 audience-slug-reserved`. | Field | Description | | --- | --- | | `kind` (string, required) | `fixed` or `rule`. Anything else returns `400 audience-kind-invalid`. | | `name` (string) | Display name, 1 to 200 characters. Defaults to the slug on create. | | `identities` (array) | The identity ids a `fixed` audience reaches. Required for `fixed`; an empty list reaches nobody. An id with no record is created. Sent with `kind: rule` it returns `400 audience-kind-mismatch`. | | `rule` (object) | The rule of a `rule` audience. Required for `rule`. Sent with `kind: fixed` it returns `400 audience-kind-mismatch`. | A rule is `{ match, conditions }`. `match` is `all` or `any`. `conditions` holds 1 to 20 objects of the form `{ trait, operator, values }`. `operator` is `is`, `is_not`, `is_one_of`, `is_not_one_of`, `exists` or `does_not_exist`. `is` and `is_not` take one value, `is_one_of` and `is_not_one_of` take 1 to 50, and `exists` and `does_not_exist` take none. A rule that breaks these limits returns `400 audience-rule-invalid`. A replace that changes the kind must carry the definition of the new kind, or it returns `400 audience-kind-switch-invalid`. ```curl curl -X PUT https://api.past.dev/api/v1/audiences/acme \ -H "Authorization: Bearer $PAST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "kind": "rule", "name": "Acme", "rule": { "match": "all", "conditions": [ { "trait": "company", "operator": "is", "values": ["acme"] } ] } }' ``` ```201created { "slug": "acme", "name": "Acme", "kind": "rule", "memberCount": 3, "memoryCount": 0, "reachesNobody": false, "recomputedAt": "2026-09-15T10:00:00Z", "createdAt": "2026-09-15T10:00:00Z", "identities": null, "rule": { "match": "all", "conditions": [ { "trait": "company", "operator": "is", "values": ["acme"] } ] } } ``` `memberCount` is the number of identities the audience reaches now. `memoryCount` is the number of memories scoped to it. `reachesNobody` is true for a rule that matches no identity. `recomputedAt` is when the members were last computed, null for `project`. A `fixed` audience returns its `identities` sorted and a null `rule`; a `rule` audience returns its `rule` and null `identities`. ### GET /api/v1/audiences/{slug} Return one audience. Project key. The slug `project` returns the default audience. An unknown slug returns `404 not-found`. ```curl curl https://api.past.dev/api/v1/audiences/acme \ -H "Authorization: Bearer $PAST_API_KEY" ``` ### DELETE /api/v1/audiences/{slug} Delete one audience. Project key. The memory scoped to it stays and reaches nobody, and the slug is free again; an audience created later under the same slug does not reach that memory. Returns `204` with no body. The slug `project` returns `400 audience-slug-reserved`, and an unknown slug returns `404 not-found`. ```curl curl -X DELETE https://api.past.dev/api/v1/audiences/acme \ -H "Authorization: Bearer $PAST_API_KEY" ``` ### POST /api/v1/audiences/{slug}/recompute Recompute the members of one audience from its definition, apply the result to recall now, and return the refreshed record with `200`. Project key. The slug `project` returns `400 audience-slug-reserved`. ```curl curl -X POST https://api.past.dev/api/v1/audiences/acme/recompute \ -H "Authorization: Bearer $PAST_API_KEY" ``` ### GET /api/v1/projects List the organization's projects, archived ones included. Management key. `ingestionCap` and `creditCap` are the project's monthly caps on data points and credits, null when the project carries none, set under Settings, Projects in the console. `monthIngestions` and `monthCredits` are the current calendar month in UTC. A reached cap returns `429 project-cap-reached` on sends. ```curl curl https://api.past.dev/api/v1/projects \ -H "Authorization: Bearer $PAST_MANAGEMENT_KEY" ``` ```200ok { "projects": [ { "slug": "acme", "name": "Acme", "description": "", "isArchived": false, "memoryCount": 41, "ingestionCap": 50000, "monthIngestions": 1204, "creditCap": null, "monthCredits": 3610, "createdAt": "2026-09-15T10:00:00Z" } ] } ``` ### POST /api/v1/projects Create a project. Management key. Returns `201` with the project record. The slug is the name in lowercase with dashes for spaces and punctuation; read it from the response, since it never changes. A project past the plan's count, archived projects included, returns `402 project-limit-reached`. | Field | Description | | --- | --- | | `name` (string, required) | Display name, up to 255 characters with at least two letters or digits. Anything else returns `400 project-name-invalid`. | ```curl curl -X POST https://api.past.dev/api/v1/projects \ -H "Authorization: Bearer $PAST_MANAGEMENT_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme" }' ``` ```201created { "slug": "acme", "name": "Acme", "description": "", "isArchived": false, "memoryCount": 0, "ingestionCap": null, "monthIngestions": 0, "creditCap": null, "monthCredits": 0, "createdAt": "2026-09-15T10:00:00Z" } ``` ### POST /api/v1/projects/{slug}/keys Mint a project key for one project. Management key. Returns `201` with the key record; `key` is the clear value, returned in this response only. The console can reveal it again. A slug the organization does not hold returns `404 not-found`. | Field | Description | | --- | --- | | `name` (string, required) | Key name, 1 to 64 characters. Anything else returns `400 key-name-invalid`. | ```curl curl -X POST https://api.past.dev/api/v1/projects/acme/keys \ -H "Authorization: Bearer $PAST_MANAGEMENT_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme production" }' ``` ```201created { "id": "k7Qx2", "name": "Acme production", "keyPrefix": "past_sk_7f", "keySuffix": "3d9a", "createdAt": "2026-09-15T10:00:00Z", "key": "past_sk__" } ``` ### DELETE /api/v1/projects/{slug}/keys/{keyId} Revoke a project key. Management key. `keyId` is the `id` from the create response. The key is refused from its next call. Returns `204` with no body. A key that is not a live key of that project returns `404 not-found`. ```curl curl -X DELETE https://api.past.dev/api/v1/projects/acme/keys/k7Qx2 \ -H "Authorization: Bearer $PAST_MANAGEMENT_KEY" ``` ### GET /api/v1/requests List this project's API calls, newest first, as the console's Requests screen lists them. Project key. Every call is here, accepted or refused, with its status, its outcome, what it was billed and the ingestion it started. The debugging reads themselves are not: listing your requests shows what your application sent, never the calls you made to look. A range wider than 31 days returns `400 range-too-wide`, and a range that starts after it ends returns `400 invalid-range`. - `from`, `to`: the window, ISO 8601. The past 24 hours when omitted, 31 days at most. - `status`: exact status codes to keep, repeatable, for example `status=401`. - `statusClass`: `2xx`, `4xx` or `5xx`, repeatable. - `endpoint`: routes to keep, as routed, repeatable. - `keyName`: key names to keep, repeatable. - `search`: matches a request id, a key name or a data point id. - `after`: the `endCursor` of the previous page. - `first`: page size, 1 to 100, default 25. ```curl curl "https://api.past.dev/api/v1/requests?statusClass=4xx&first=25" \ -H "Authorization: Bearer $PAST_API_KEY" ``` ```200ok { "project": "acme", "from": "2026-09-14T10:00:00Z", "to": "2026-09-15T10:00:00Z", "totalCount": 1, "hasMore": false, "endCursor": null, "payloadRetentionDays": 30, "requests": [ { "requestId": "req_7f3a9c21", "receivedAt": "2026-09-15T10:00:00Z", "method": "POST", "path": "/api/v1/ingest", "status": 202, "errorCode": null, "outcome": "Accepted", "durationMs": 38, "key": { "name": "production", "prefix": "past_sk_9f26" }, "identity": null, "ingestionId": "7d9f2b6a-4c1e-4f8a-9b3d-2e5c8a1f6d40", "phase": "indexed", "credits": 12 } ] } ``` ### GET /api/v1/requests/histogram Calls per time bucket over a window, split by how they ended: `accepted`, `failedLater` for a call the platform accepted and then stopped on, and `rejected`. Project key. It takes the same window and filters as the list, and `bucketMinutes` names the interval the platform chose. `totals` sums the buckets. ```curl curl "https://api.past.dev/api/v1/requests/histogram?from=2026-09-14T00:00:00Z&to=2026-09-15T00:00:00Z" \ -H "Authorization: Bearer $PAST_API_KEY" ``` ```200ok { "project": "acme", "from": "2026-09-14T00:00:00Z", "to": "2026-09-15T00:00:00Z", "bucketMinutes": 60, "totals": { "accepted": 412, "failedLater": 1, "rejected": 3 }, "buckets": [ { "start": "2026-09-14T00:00:00Z", "accepted": 17, "failedLater": 0, "rejected": 0 } ] } ``` ### GET /api/v1/requests/{requestId} One call in full, as the console's request drawer shows it. Project key. It carries the list's fields plus the bytes, the allowlisted request headers and the stored request and response bodies, while the plan's payload retention keeps them: past that window `payloadExpired` is true and `payload` is null. The bearer key is never stored, and never returned. An unknown request id, or one from another project, returns `404 not-found`. ```curl curl https://api.past.dev/api/v1/requests/req_7f3a9c21 \ -H "Authorization: Bearer $PAST_API_KEY" ``` ### GET /api/v1/trace/roots Find a memory or a source to trace, in two paged groups, newest first. Project key. A source matches on its text and on its data point id, a memory on its own words and on its id. Pass `ingestionId` to list what one send carried and the memories drawn from it. Every entry's `id` is what the lineage and timeline reads take as a root. - `search`: text, a data point id or a memory id. - `ingestionId`: narrow both groups to what one send carried. - `first`: entries per group, 1 to 50, default 25. - `afterMemories`, `afterSources`: each group's `endCursor` from the previous page. ```curl curl "https://api.past.dev/api/v1/trace/roots?search=pilot" \ -H "Authorization: Bearer $PAST_API_KEY" ``` ### GET /api/v1/trace/nodes/{id} One memory or source by its id. Project key. `kind` is `memory` or `source`. The answer carries the headline, the data point and revision behind it, when it occurred, arrived and became searchable, the period it is valid for, whether it is the current version and which id it replaced, and how many inputs and outputs it has. A source carries the text you sent, cut to 4,000 characters with `contentTruncated` saying so. An id this project does not hold returns `404 not-found`. ```curl curl https://api.past.dev/api/v1/trace/nodes/b1c2d3e4-5f60-4a7b-8c9d-0e1f2a3b4c5d \ -H "Authorization: Bearer $PAST_API_KEY" ``` ```200ok { "id": "b1c2d3e4-5f60-4a7b-8c9d-0e1f2a3b4c5d", "kind": "source", "headline": "Acme runs the pilot in Q4.", "memoryKind": null, "dataPointId": "call-8821", "revision": 1, "occurredAt": "2026-09-15T09:58:00Z", "createdAt": "2026-09-15T10:00:01Z", "readyAt": "2026-09-15T10:00:06Z", "validFrom": null, "validTo": null, "current": true, "replacedId": null, "inputCount": 0, "outputCount": 3, "phase": "indexed", "requestId": "req_7f3a9c21", "content": "Acme runs the pilot in Q4.", "contentTruncated": false } ``` ### GET /api/v1/trace/lineage/{rootId} What a memory was built from, and what was built on it. Project key. This is the answer to "the system claims this, where did it come from": the nodes around the root and the edges among them, where every edge reads as `derivedId` was built from `inputId`. `hiddenCount` says how many nodes the limit left out. A root this project does not hold returns `404 not-found`. - `revealAll`: walk every side of every node up to the limit. `false` walks only `reveal`, or the root's inputs when that is empty. - `reveal`: sides to open, each `:inputs` or `:outputs`, repeatable. Anything else returns `400 invalid-reveal`. - `limit`: the most nodes to return, 1 to 400, default 400. ```curl curl "https://api.past.dev/api/v1/trace/lineage/b1c2d3e4-5f60-4a7b-8c9d-0e1f2a3b4c5d?revealAll=true&limit=50" \ -H "Authorization: Bearer $PAST_API_KEY" ``` ```200ok { "root": { "id": "b1c2d3e4-5f60-4a7b-8c9d-0e1f2a3b4c5d", "kind": "source", "headline": "Acme runs the pilot in Q4." }, "nodes": [ { "id": "9a8b7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d", "kind": "memory", "memoryKind": "claim", "headline": "Acme's pilot runs in Q4." } ], "edges": [ { "inputId": "b1c2d3e4-5f60-4a7b-8c9d-0e1f2a3b4c5d", "derivedId": "9a8b7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d" } ], "hiddenCount": 0 } ``` ### GET /api/v1/trace/timeline/{rootId} The same work on a clock. Project key. Every node around the root with when it occurred, arrived and became searchable, each in the first section that claims it: `revision`, `built-from`, `produced` or `elsewhere`. Beside the rows, the sends in the window that carried its sources with their phase, the recalls that returned one of the rows while the payload retention keeps them, and which memory replaced which. `from` and `to` bound the window, at most 31 days, and default to the root's recent history. ```curl curl "https://api.past.dev/api/v1/trace/timeline/9a8b7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d?from=2026-09-01T00:00:00Z" \ -H "Authorization: Bearer $PAST_API_KEY" ``` ### GET /api/v1/usage This calendar month's credits, in UTC. Project key or management key, and the answer follows the key: a project key reads the organization's balance and its own project's month, the management key reads the balance and every live project, heaviest spender first. `metered` is false on a deployment with no plan, and the credit figures are then null while the caps, which bind either way, are still answered. `resetsAt` is null where nothing resets. The read spends nothing and leaves no row in the request log. ```curl,projectkey curl https://api.past.dev/api/v1/usage \ -H "Authorization: Bearer $PAST_API_KEY" ``` ```200ok { "month": { "from": "2026-09-01T00:00:00Z", "to": "2026-09-15T10:00:00Z", "resetsAt": "2026-10-01T00:00:00Z" }, "balance": { "left": 27340, "included": 30000, "bought": 0, "rolledOver": 0, "spent": 2660 }, "project": { "slug": "acme", "credits": 2660, "ingestions": 412, "recalls": 1804, "creditCap": null, "creditCapLeft": null, "ingestionCap": null, "ingestionCapLeft": null }, "metered": true } ``` # When something looks wrong > Four reads answer the questions an integration raises: where a send got to, what a memory was built from, what an import will cost, and what is left to spend. Product: past.dev Memory API. Source: https://past.dev/docs/memory-api/debugging Everything on this page is a read of your own project, over the same project key your application already holds. A debugging read spends no credits and leaves no row in the request log, so listing your requests shows what your application sent and never the calls you made to look. They count against the plan's requests per minute like any other call, except `GET /api/v1/usage`, which is outside that budget. ### My data is not showing up Start with `GET /api/v1/ingest/{ingestionId}/explain`. It answers in one call where the send got to and what to do about it, so there is no sequence of reads to assemble. - `queued`, `processing` or `captured`: the send is still moving. Text becomes searchable within seconds of arriving, and the memories drawn from it follow within minutes. Wait. - `nothing-changed`: every data point carried content the project already held. Send different content, or a new revision of it. - `indexed` with no memories on an item: the content held nothing the platform keeps as a memory. Recall still finds its text. - `failed`: the send stopped. `failure.code` says why and `failure.retryable` says whether running it again can fix it. For a send that stopped, `POST /api/v1/ingest/{ingestionId}/retry` runs it again under the same ingestion id, free. The one reason a retry does not fix is `content-rejected`, where a step refused the content itself: change the content and send it as a new revision. Sending the same content again is not a way back, because an unchanged data point is not processed twice. If the send never reached the platform at all, the call is in `GET /api/v1/requests` with the status and the error code it answered, and the Errors page above says what each code means. ### Where did this memory come from Find the memory with `GET /api/v1/trace/roots`, which searches memories and sources by text or by id, then read `GET /api/v1/trace/lineage/{rootId}`. The lineage gives you the nodes around the root and the edges among them, each edge reading as built from, so a claim you doubt leads back to the data point you sent, and a data point leads forward to everything drawn from it. `GET /api/v1/trace/nodes/{id}` opens one of them, and a source carries the text you sent. `GET /api/v1/trace/timeline/{rootId}` lays the same work on a clock: when each memory arrived and became searchable, which sends carried its sources, which recalls returned it, and which memory replaced which. That is the read for what past knew at a given moment, and for how long a memory you have since corrected was live. ### What will this import cost `POST /api/v1/ingest/estimate` takes the files and answers what they become before anything is sent: the kind each file was read as, the data points it cuts into, the batches to send them in with their idempotency keys, and the credits. `fits` and `fitsFirst` compare the whole import to what is left of your credits and of the project's cap, so a run that cannot pay for all of it can send the oldest part and stop. The cut is the same one the console's API Explorer shows you before a drop is sent, and the same one described under Sending data: one message per email, one day per chat, one section per document, one row per table. Sending the points the estimate returned is the way to get the recall these docs describe. ### Why was my send refused A `402 out-of-credits` or a `429 project-cap-reached` is a question about the month, and `GET /api/v1/usage` answers it: the credits left, what the month included and bought, the reset date, and what is left of this project's caps. > **The same reads from an assistant** > > Every read on this page is also an account MCP server tool, so Claude or Cursor can work out what went wrong without leaving the conversation: `explain_ingestion`, `find_trace_roots`, `get_trace_node`, `get_lineage`, `get_timeline`, `estimate_ingestion`, `get_credit_balance`, and `retry_ingestion`, which asks you to confirm first. # Errors > Every refusal uses a JSON body with code, status, debugMessage, and optional data, except the per-minute limit, which answers with no body. Product: past.dev Memory API. Source: https://past.dev/docs/memory-api/errors ```400badrequest { "code": "content-required", "status": 400, "debugMessage": "Content is required", "data": null } ``` | Response | When | | --- | --- | | **400** (content-required) | A single send or one batch item has empty content. | | **400** (invalid-ingestion) | The send or batch shape is invalid, including an empty batch, duplicate source ids, or more than 1,000 items. | | **400** (audience-unknown) | The requested audience slug does not exist in the project. | | **400** (audience-ids-unsupported) | The retired integer-array audience form was sent instead of a slug. | | **400** (audience-slug-invalid) | The slug is outside 2 to 40 lowercase letters, digits and dashes with a letter or digit at each end. | | **400** (audience-slug-reserved) | The slug `project` was sent to a replace, delete or recompute. | | **400** (audience-name-invalid) | The audience name is empty or over 200 characters. | | **400** (audience-kind-invalid) | `kind` is neither `fixed` nor `rule`. | | **400** (audience-identities-required / audience-rule-required) | A `fixed` audience was sent without `identities`, or a `rule` audience without `rule`. | | **400** (audience-kind-mismatch) | `identities` was sent with `kind: rule`, or `rule` with `kind: fixed`. | | **400** (audience-kind-switch-invalid) | A replace changes the kind without the definition of the new kind. | | **400** (audience-rule-invalid) | A rule has no condition or more than 20, an unknown operator, the wrong number of values, or a blank value. | | **400** (identity-invalid) | An identity or trait does not satisfy its wire constraints. | | **400** (traits-invalid) | A trait value is neither a string nor null. | | **400** (identity-required) | Recall omitted `identity`. | | **400** (recall-query-required) | Recall omitted a non-empty `query`. | | **400** (recall-sort-invalid) | Recall `sort` is neither `relevance` nor `chronological`. | | **400** (recall-range-invalid) | Recall `occurredFrom` is after `occurredTo`. | | **400** (recall-level-invalid) | Recall `level` is not `low`, `medium`, `high` or `extra-high`. | | **400** (recall-level-conflict) | Recall sent both `level` and `maxTokens`. | | **400** (recall-budget-invalid) | Recall `maxTokens` is not a positive number. | | **400** (invalid-deletion) | A deletion names no id or more than 1,000 ids. | | **400** (bulk-empty / bulk-too-large / bulk-invalid-rows) | An identity bulk request is empty, over 1,000 rows, or contains malformed rows. | | **400** (project-required) | A management key called an identity route without `?project=`. | | **400** (project-mismatch) | A project key called an identity route with a `?project=` that names another project. | | **400** (project-name-invalid) | The project name is empty, over 255 characters, or has fewer than two letters or digits. | | **400** (key-name-invalid) | The key name is empty or over 64 characters. | | **401** (missing-api-key) | No `Authorization` header, or `Bearer` with no value. | | **401** (malformed-api-key) | The header does not read `Bearer `, or the bearer is not a past key. `debugMessage` says which. | | **401** (unauthorized) | The key is unknown, expired or revoked, or of the kind this route refuses. | | **402** (out-of-credits) | The organization's credits are spent. Sends stop on every plan, and recalls stop too on Pay as you go, which charges for them. Do not retry. | | **402** (project-limit-reached) | The plan's projects are all in use, archived projects included. Delete a project or upgrade the plan. | | **403** (project-archived) | The key's project is archived. Unarchive it in the console to resume. | | **400** (range-too-wide) | A request or timeline window is wider than 31 days. | | **400** (invalid-range) | A request or timeline window starts after it ends. | | **400** (invalid-reveal) | A lineage `reveal` is not `:inputs` or `:outputs`. | | **400** (invalid-estimate) | An estimate sent both `files` and `items`, or neither, or carries more than 10,000 files, a repeated path or a repeated source id. | | **400** (invalid-kind) | An estimate names a kind that is not `email`, `chat`, `transcript`, `table`, `records`, `code`, `documentation` or `notes`. | | **400** (invalid-points) | An estimate's `points` is not `none`, `summary` or `full`. | | **400** (invalid-time-zone) | An estimate's `timeZone` is not an IANA name such as `Europe/Paris`. | | **404** (not-found) | The requested ingestion, request, memory, source, identity, audience, project or key does not exist in the project. | | **404** (data-point-not-found) | A deletion names an id with no accepted, undeleted data point in the project. Nothing is deleted. | | **409** (idempotency-conflict) | The same idempotency key was used with another body. | | **409** (ingestion-not-stopped) | A retry named a send that has not stopped. It is left as it was. | | **409** (conflict) | An identity bulk request raced a concurrent write. Nothing was applied; retry the batch. | | **409** (audience-slug-taken) | Two creates raced on the same slug. Send the request again. | | **413** (content-too-large) | The content of one request exceeds 16 MiB of UTF-8, a single send included. Split it into smaller requests. | | **429** (project-cap-reached) | The project's monthly cap, set in the console, is reached. | | **429** (no body) | The plan's requests per minute are exceeded for this key. This refusal carries no JSON body. Wait and retry. | | **503** (organization-not-ready) | The organization database is not ready. Retry within minutes. | | **503** (pipeline-disabled) | Background ingestion is disabled on the deployment. | Match application behavior on `code` and `status`. `debugMessage` is explanatory text and may change; a batch refusal names the item in it. `data` contains structured details only when the refusal provides them. Every refusal also carries the `X-Request-Id` header. Quote it to support@past.dev when you report a problem, and find the call under it on the console's Requests screen. # Self-hosting > Run the same API and console on your infrastructure with the published Docker Compose bundle. Product: past.dev Memory API. Source: https://past.dev/docs/memory-api/self-hosting The bundle provides the same public routes as the managed service. Replace `https://api.past.dev` with your deployment's host in every sample. Follow the bundle's deployment guide to enable ingestion before sending data; otherwise ingest routes return `503 pipeline-disabled`. `GET /health` returns `status`, `name`, and `version` on every deployment. A self-hosted deployment has no monthly allowance. - Pin API and console images to an exact release version. - Create a backup before upgrading. - Run the release health checks before sending traffic. - Use the bundle's version-specific restore instructions for rollback. > **Getting the bundle** > > Ask us for the self-host bundle. It includes configuration, upgrade, backup, and restore instructions.