--- title: "Endpoint reference" description: "Thirty-four operations under /api/v1. Field names match the backend JSON contract." canonical: https://past.dev/docs/memory-api/api-reference last-updated: 2026-10-09 --- # Endpoint reference > Thirty-four 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 content, label, metadata and timestamp all match what is stored for the id. A send that changes only the audience also returns `unchanged: true`: it moves the data point and processes nothing. | Field | Description | | --- | --- | | `content` (string, required) | Raw text. Empty or whitespace-only content is refused. | | `label` (string) | A short title for the data point, such as `Email from Nadia`. Memory extraction reads it beside the content, and recall returns it on the data point's sources. Omission retains the stored value for an existing id. | | `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. No key is reserved: memory extraction reads it whole as context, and recall returns it as sent. 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" }' ``` ```202accepted { "projectId": 12, "ingestionId": "7d9f2b6a-4c1e-4f8a-9b3d-2e5c8a1f6d40", "sourceId": "call-8821", "status": "queued", "unchanged": null } ``` ```200ok,unchanged { "projectId": 12, "ingestionId": "3b5e8c1d-9a24-4f6b-8e07-6c2d1a9f4b53", "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" }, { "id": "call-8822", "content": "Nadia asked for the revised scope by August 4.", "timestamp": "2026-07-29T09:30:00Z" } ] }' ``` ```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 content-too-large`. ### 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.", "content": null, "metadata": null } ], "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 }, "failure": null } ``` - `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/webhooks/{webhookId}/{token} Receive one event from another system. No key: the token in the URL is the credential. The console gives the complete URL when the webhook is created, and the sample holds it in `WEBHOOK_URL`. The body is the sender's own, 1 MiB or less, with any `Content-Type`. Returns `202` with `verdict` `ingested` when the event becomes a data point, or `200` with `verdict` `dropped` when a filter or the content mapping drops it. [Webhooks](/docs/memory-api/webhooks) describes the mapping, the filters and the credits. ```curl curl -X POST "$WEBHOOK_URL" \ -H "Content-Type: application/json" \ -d '{ "webhookEvent": "jira:issue_updated", "timestamp": 1790000000000, "issue": { "key": "SUP-1042", "fields": { "summary": "Export fails on projects over 50,000 rows." } } }' ``` ```202accepted { "eventId": "evt_k3m7q2xw5n6r4t2v", "requestId": "req_h3k7m2qx5nwr", "verdict": "ingested", "droppedBy": null, "ingestionId": "7d9f2b6a-4c1e-4f8a-9b3d-2e5c8a1f6d40", "dataPointId": "SUP-1042", "unchanged": null } ``` `requestId` is the value of the `X-Request-Id` header. `dataPointId` is the mapped id, or `eventId` when the webhook maps none, and `ingestionId` is the id that `GET /api/v1/ingest/{ingestionId}` takes. On a dropped event, `droppedBy` names the filter or the missing content, and `ingestionId`, `dataPointId` and `unchanged` are null. A repeat of an unchanged data point returns `202` with `unchanged: true`. A wrong id or token, or a deleted webhook, returns `404 not-found`. A paused webhook returns `409 webhook-paused`, and a body over 1 MiB returns `413 webhook-body-too-large`. A plan without webhooks returns `402 webhooks-not-included`. Spent credits and reached caps return the refusals of a send. An event past the limit of 600 events in one minute for one webhook returns `429` with no body. ### 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 100 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 Pay as you go 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", "label": "Email from Nadia", "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`, `claim`, `rule`, `membership`, `identity`, `mention` or `temporal-anchor`. [Concepts](/docs/memory-api/concepts#memory) defines each kind and the form of its `content`. 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 `label` 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", "audiences": null } ``` ### 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", "audiences": null } ], "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`, `console`, `login` or `link`, 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": 147340, "included": 150000, "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 } ```