Authentication
All API requests require a Bearer token in the Authorization header. Generate API keys from your dashboard.
Authorization: Bearer rec_live_<your_api_key>
# Example curl (search uses POST with JSON body)
curl -X POST https://api.retainr.dev/v1/memories/search \
-H "Authorization: Bearer rec_live_abc123..." \
-H "Content-Type: application/json" \
-d '{"query":"customer preferences","namespace":"customer:alice","limit":5}'Key format: API keys start with rec_live_ followed by 32 base58 characters. Store them in environment variables - never hardcode in workflow nodes.
Quick start - 2 API calls to memory
The core loop is: store what your agent learns, search for relevant context before responding. Nothing else required.
// POST https://api.retainr.dev/v1/memories
{
"content": "Customer prefers email over phone calls and is on a budget",
"namespace": "customer:alice",
"tags": ["preference", "contact-method"]
}// POST https://api.retainr.dev/v1/memories/search
{
"query": "how should I contact this customer?",
"namespace": "customer:alice",
"limit": 5
}
// Response
{
"results": [
{
"id": "mem_8xKp2...",
"content": "Customer prefers email over phone calls and is on a budget",
"score": 0.94,
"namespace": "customer:alice",
"tags": ["preference", "contact-method"],
"created_at": "2026-03-10T09:00:00Z"
}
],
"total": 1
}Store a memory
/v1/memoriesEmbed and store a memory. Namespace is the sole organizing parameter.Request body
| Field | Type | Required | Description |
|---|---|---|---|
content | string | Yes | The text to embed and store. Max 32,768 characters. |
namespace | string | Yes | Grouping key. Any string works: 'customer:alice', 'project:q1', 'session:xyz'. The sole way to partition memories across users, tenants, or workflows. |
tags | string[] | No | Up to 20 tags for filtering. E.g. ['preference', 'support']. |
ttl_seconds | integer | No | Seconds until this memory auto-deletes. Max 31,536,000 (1 year). Omit for permanent storage. |
dedup_threshold | number | No | Cosine similarity (0–1). If an existing memory in the same namespace matches this threshold, it is updated instead of duplicated. 0 = always insert. |
metadata | object | No | Arbitrary JSON metadata attached to the memory. |
Example request
{
"content": "Customer opened a refund ticket for order #8821. Issue: wrong size delivered.",
"namespace": "customer:acme",
"tags": ["support", "refund", "order-issue"],
"ttl_seconds": 2592000,
"dedup_threshold": 0.92
}Response - 201 Created (new) / 200 OK (deduplicated)
{
"id": "mem_8xKp2mQ4nR...",
"workspace_id": "ws_01JK...",
"namespace": "customer:acme",
"ttl_at": "2026-04-10T09:00:00Z",
"created_at": "2026-03-10T09:00:00Z",
"deduplicated": false
}Search memories
/v1/memories/searchSemantic similarity search — returns memories ranked by meaning, not keyword overlapRequest body
| Field | Type | Required | Description |
|---|---|---|---|
query | string | Yes | Natural language query. E.g. 'what does this customer prefer?' |
namespace | string | No | Filter to a namespace, e.g. 'customer:acme'. |
tags | string[] | No | Tag filter array. Only memories with ALL specified tags are returned. |
limit | integer | No | Max results. Default: 10. Max: 100. |
threshold | number | No | Minimum cosine similarity (0–1). Default: 0.5. |
Example request
{
"query": "how should I approach this customer?",
"namespace": "customer:acme",
"limit": 3,
"tags": ["preference"]
}Response - 200 OK
{
"results": [
{
"id": "mem_8xKp2mQ4nR...",
"content": "Customer prefers email over phone calls and is on a budget",
"score": 0.94,
"namespace": "customer:acme",
"tags": ["preference", "contact-method"],
"metadata": {},
"created_at": "2026-03-10T09:00:00Z"
},
{
"id": "mem_3fLm9nB2kS...",
"content": "Customer prefers concise replies — skip long explanations",
"score": 0.87,
"namespace": "customer:acme",
"tags": ["preference"],
"metadata": {},
"created_at": "2026-03-09T14:22:00Z"
}
],
"total": 2
}List memories
/v1/memoriesPaginated list of all memories in your workspace with optional filtersQuery parameters
| Parameter | Type | Description |
|---|---|---|
namespace | string | Filter by namespace, e.g. 'customer:acme'. |
tags | string | Comma-separated. Returns memories containing ALL specified tags. |
limit | number | Page size. Default: 20. Max: 100. |
offset | number | Number of records to skip. Default: 0. |
Response - 200 OK
{
"memories": [
{
"id": "mem_8xKp2mQ4nR...",
"content": "Customer is price-sensitive, prefers email",
"namespace": "customer:acme",
"tags": ["preference"],
"metadata": {},
"created_at": "2026-03-10T09:00:00Z"
}
],
"total": 47,
"limit": 20,
"offset": 0
}Build context window
/v1/memories/contextRetrieve relevant memories pre-formatted for injection into an AI system promptThe context endpoint does the semantic search and formats the result as ready-to-inject text. Paste the returned context string directly into your LLM's system prompt.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
query | string | Yes | The current user message or topic to retrieve context for. |
namespace | string | No | Filter to a namespace, e.g. 'customer:acme'. |
tags | string[] | No | Only include memories with these tags. |
limit | integer | No | Max memories to include. Default: 5. Max: 20. |
threshold | number | No | Min similarity score (0–1). Default: 0.35. |
format | string | No | Output format: system_prompt (default), bullet_list, or numbered_list. |
Example request
{
"query": "How should I greet this customer?",
"namespace": "customer:acme",
"limit": 5,
"format": "system_prompt"
}Response - 200 OK
{
"context": "User context (3 memories):\n- Customer is price-sensitive, prefers email over phone calls (4 days ago)\n- Opened refund ticket for order #8821 (2 days ago)\n- Resolved: issued full refund (1 day ago)",
"memories_used": 3,
"token_estimate": 48
}Tip: Prepend the context string to your LLM's system prompt. Use token_estimate to stay within model context limits. The system_prompt format produces the most natural results.
Export memories
/v1/memories/exportDownload all workspace memories as a JSONL file — for backups, audits, or migrationsReturns a application/x-ndjson stream with one JSON object per line. Accepts the same filters as GET /v1/memories. Embedding vectors are never included in the export.
Example request
# Download full workspace export
curl https://api.retainr.dev/v1/memories/export \
-H "Authorization: Bearer rec_live_..." \
-o memories.jsonl
# Export only a specific namespace
curl "https://api.retainr.dev/v1/memories/export?namespace=customer:acme" \
-H "Authorization: Bearer rec_live_..." \
-o acme_memories.jsonlResponse format (JSONL)
{"id":"mem_8xKp2...","content":"Customer prefers email","namespace":"customer:acme","tags":["preference"],"created_at":"2026-03-10T09:00:00Z"}
{"id":"mem_3fLm9...","content":"Opened refund ticket for order #8821","namespace":"customer:acme","tags":["support"],"created_at":"2026-03-08T14:22:00Z"}Delete memories
/v1/memoriesBulk-delete memories by namespace or tags - scoped to your workspaceAll deletes are scoped to your workspace. You cannot delete memories belonging to another workspace. Deletion is permanent and immediate - no soft deletes.
Request body
| Field | Type | Description |
|---|---|---|
namespace | string | Delete all memories in a namespace, e.g. 'customer:acme'. At least one filter is required. |
tags | string[] | Delete memories matching ALL specified tags. |
Example - delete all memories for a namespace (GDPR erasure)
{
"namespace": "customer:acme"
}Response - 200 OK
{
"deleted": 14
}Webhooks
Webhooks push real-time events to your URL when memories are stored or match a semantic watch query. Every delivery is signed — verify the X-Retainr-Signature header to confirm authenticity.
Fires on every new memory stored in your workspace — regardless of content. Use for audit trails, CRM sync, or triggering downstream actions on every store.
Fires only when a new memory's semantic similarity to your watch_query meets the threshold. Use for keyword monitoring, churn detection, or intent alerts.
/v1/webhooksRegister a webhook endpoint for your workspace.Request body
{
"name": "string (required) — human label, e.g. 'CRM sync'",
"url": "string (required) — must be https://",
"event_type": "string (required) — 'memory.created' or 'memory.watch'",
"namespace_filter": "string (optional) — only fire for memories in this namespace, e.g. 'customer:acme'",
"watch_query": "string (required for memory.watch) — phrase to match against",
"watch_threshold": "number (optional, default 0.75) — min cosine similarity 0.5–1.0",
"secret": "string (optional) — HMAC secret for signature; auto-generated if omitted"
}Example — watch for churn signals
POST /v1/webhooks
Authorization: Bearer rec_live_...
{
"name": "Churn alert",
"url": "https://hook.make.com/abc123",
"event_type": "memory.watch",
"watch_query": "cancel subscription unsubscribe want to leave",
"watch_threshold": 0.8,
"namespace_filter": "customer:acme"
}Response — 201 Created
{
"id": "wh_01JKABCDEF",
"workspace_id": "ws_01JK...",
"name": "Churn alert",
"url": "https://hook.make.com/abc123",
"event_type": "memory.watch",
"watch_query": "cancel subscription unsubscribe want to leave",
"watch_threshold": 0.8,
"enabled": true,
"secret": "a3f8...",
"created_at": "2026-03-14T12:00:00Z"
}/v1/webhooksList all webhooks registered in your workspace.Response — 200 OK
[
{
"id": "wh_01JKABCDEF",
"name": "Churn alert",
"url": "https://hook.make.com/abc123",
"event_type": "memory.watch",
"watch_query": "cancel subscription",
"watch_threshold": 0.8,
"enabled": true,
"consecutive_failures": 0,
"created_at": "2026-03-14T12:00:00Z",
"updated_at": "2026-03-14T12:00:00Z"
}
]/v1/webhooks/{id}Update a webhook — change URL, watch query, threshold, or toggle enabled/disabled. All fields are optional; only provided fields are updated.Request body (all optional)
{
"name": "string",
"url": "https://...",
"watch_query": "string",
"watch_threshold": 0.75,
"enabled": true
}Returns 204 No Content on success. Re-enabling a webhook (enabled: true) resets consecutive_failures to 0.
/v1/webhooks/{id}Permanently delete a webhook. Deliveries in-flight are not cancelled.Returns 204 No Content on success.
/v1/webhooks/{id}/testSend a test delivery to the webhook URL with a synthetic memory.created payload. Useful for verifying your endpoint before going live.Response — 200 OK
{ "delivered": true, "http_status": 200 }
// or on connection failure:
{ "delivered": false, "error": "dial tcp: connection refused" }Payload format & signature verification
memory.created payload
{
"event": "memory.created",
"webhook_id": "wh_01JKABCDEF",
"workspace_id": "ws_01JK...",
"fired_at": "2026-03-14T15:04:05Z",
"memory": {
"id": "mem_01JK...",
"content": "User prefers email over SMS",
"namespace": "customer:acme",
"created_at": "2026-03-14T15:04:05Z",
"metadata": {}
}
}memory.watch payload (adds match object)
{
"event": "memory.watch",
"webhook_id": "wh_01JKABCDEF",
"workspace_id": "ws_01JK...",
"fired_at": "2026-03-14T15:04:05Z",
"memory": { ... },
"match": {
"score": 0.91,
"watch_query": "cancel subscription unsubscribe want to leave"
}
}Verifying the signature
Every delivery includes an X-Retainr-Signature header. Compute sha256=HMAC-SHA256(secret, raw_body) and compare with constant-time equality. Same pattern as Stripe and GitHub webhooks.
// Node.js verification example
const crypto = require('crypto')
function verifySignature(secret, rawBody, sigHeader) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(sigHeader)
)
}
// Express / Make.com custom webhook handler
app.post('/hook', express.raw({ type: 'application/json' }), (req, res) => {
if (!verifySignature(process.env.RETAINR_SECRET, req.body, req.headers['x-retainr-signature'])) {
return res.status(401).send('Invalid signature')
}
const event = JSON.parse(req.body)
console.log(event.event, event.memory.content)
res.sendStatus(200)
})Auto-disable: Webhooks are automatically disabled after 10 consecutive delivery failures. Re-enable via PUT /v1/webhooks/{id} with { "enabled": true }. Failed deliveries can be retried individually from the dashboard.
Rate limits
Rate limits are enforced per API key, per minute. Memory operations count against your monthly plan quota separately from per-minute limits.
| Plan | Requests/min | Memory ops/month | Max content size |
|---|---|---|---|
| Free | 30 | 1,000 | 4 KB |
| Builder | 120 | 20,000 | 8 KB |
| Pro | 300 | 100,000 | 8 KB |
| Agency | 600 | 500,000 | 16 KB |
When rate limited, the API returns 429 Too Many Requests with a Retry-After header indicating seconds to wait. n8n and Make.com modules handle retry automatically.
Error codes
All errors return a JSON body with error and message fields.
| Status | Error code | Meaning & resolution |
|---|---|---|
| 400 | invalid_request | Malformed JSON or missing required fields. Check the request body against the schema above. |
| 401 | unauthorized | Missing or invalid API key. Check your Authorization header. |
| 403 | forbidden | Your API key doesn't have access to this workspace. |
| 404 | not_found | The requested memory does not exist. |
| 422 | content_too_large | Content exceeds the per-plan character limit. Summarize or chunk the content. |
| 429 | rate_limited | Exceeded per-minute or monthly quota. Check Retry-After header and upgrade if needed. |
| 500 | internal_error | Unexpected server error. We log all 500s automatically. If it persists, open a support ticket. |
| 503 | embedding_unavailable | Embedding provider temporarily unavailable. Retry with exponential backoff. |
Platform quick starts
n8n
Install the official retainr community node. In n8n, go to Settings - Community Nodes - Install and enter n8n-nodes-retainr. Available operations: Store Memory, Search Memory, Delete Memory.
// n8n - Store Memory node (JSON body)
{
"content": "{{ $json.message }}",
"namespace": "user:{{ $json.userId }}",
"tags": ["{{ $json.intent }}"]
}
// n8n - Search Memory node (before your AI Call node)
{
"query": "{{ $json.userMessage }}",
"namespace": "user:{{ $json.userId }}",
"limit": 5
}
// Output: $json.memories - inject into your system promptMake.com
The native Make.com app is submitted for marketplace review. Install it from the Make.com app directory: search for retainr. Available modules: Store Memory, Search Memory, List Memories, Delete Memories.
// Make.com — Store Memory module
Content: {{1.message}}
Namespace: customer:{{1.userId}}
// Make.com — Search Memory module (before your AI module)
Query: {{1.userMessage}}
Namespace: customer:{{1.userId}}
Max Results: 5
// Output: bundle of memory items — map content into your AI system promptUntil the marketplace listing is live, use Make.com's HTTP module to call the REST API directly — set the URL to https://api.retainr.dev/v1/memories with Authorization: Bearer rec_live_....
Zapier
Coming soonThe official Zapier app is in development. In the meantime, use Webhooks by Zapier (available on all paid Zapier plans) to call retainr's REST API — POST to store, POST to search.
Want to be notified when it launches? Create a free account and we'll email you.
Ready to add memory to your workflows?
Free plan includes 1,000 memory ops per month. No credit card required.
Create free account