Official PHP SDK for the Memra Memory API -- persistent, searchable memory for AI agents and LLM applications. Privacy-first and EU-native.
Since 4.5.0 the SDK version tracks the Memra platform version.
- Read-your-writes: every write response includes a
revisiontoken; pass it torecall(waitForRevision:)to guarantee the write is indexed before recall runs. - Conflicts on write: create responses include
conflicts-- memories the new fact contradicts (when contradiction detection is enabled). - Token-budget recall:
recall(maxTokens:)trims results to fit a token budget. - Recall filters:
notTags,since,untilfor tag-exclusion and time-window recall. - Feedback loop: report which recalled memories were actually used via
memories()->feedback()or inline withrecall(usedIds:)-- boosts future scoring. - Entity graph API:
entities()->list()andentities()->memories()make the entity graph queryable. - Read-only scoped keys: the API now supports read-only scoped API keys -- use one with this SDK for recall-only integrations (write calls will be rejected by the server).
- PHP 8.1+
- Guzzle 7.5+ (ships with Laravel)
composer require memra/sdkThe SDK auto-discovers via Laravel's package discovery -- no manual registration needed.
Set your API key in .env:
MEMRA_API_KEY=memra_live_your_key_here
MEMRA_PROJECT_ID=my-projectOptionally publish the config file:
php artisan vendor:publish --tag=memra-configThis creates config/memra-sdk.php where you can customize base_url, timeout, and project_id.
Using
config:cache? Publishing the config file is recommended -- it guarantees the SDK's settings are part of your cached configuration. Remember to re-runphp artisan config:cacheafter installing or updating the SDK.
composer require memra/sdkAdd to your .env:
MEMRA_API_KEY=memra_live_your_key_here
MEMRA_PROJECT_ID=my-projectuse Memra\Sdk\Facades\Memra;
$memory = Memra::add(
content: 'User prefers dark mode and TypeScript',
tenantId: 'user_123',
projectId: 'my-project',
type: 'preference',
importance: 8,
tags: ['ui', 'language'],
);
echo $memory->id; // mem_01HXYZ...$results = Memra::recall(
query: 'What does the user prefer?',
tenantId: 'user_123',
projectId: 'my-project',
limit: 5,
);
foreach ($results->data as $memory) {
echo "{$memory->content} (score: {$memory->score})\n";
}Memra::delete('mem_01HXYZ');use Memra\Sdk\MemraClient;
$client = new MemraClient(apiKey: 'memra_live_your_key_here');
// Store a memory
$memory = $client->memories()->add(
content: 'User prefers dark mode',
tenantId: 'user_123',
projectId: 'my-project',
type: 'preference',
importance: 8,
);
// Recall memories
$results = $client->memories()->recall(
query: 'What does the user prefer?',
tenantId: 'user_123',
projectId: 'my-project',
);
// List memories
$list = $client->memories()->list(
tenantId: 'user_123',
projectId: 'my-project',
);
// Delete a memory
$client->memories()->delete($memory->id);// Via Facade (Laravel)
Memra::add(content, tenantId, projectId, type?, importance?, tags?, source?, metadata?);
Memra::recall(query, tenantId, projectId, limit?, types?, tags?, minImportance?, minScore?, includeRecency?, rerank?, waitForRevision?, maxTokens?, notTags?, since?, until?, usedIds?);
Memra::list(tenantId, projectId, type?, tags?, minImportance?, limit?, offset?);
Memra::delete(id);
// Via Client (Plain PHP or Laravel)
$client->memories()->add(...);
$client->memories()->recall(...);
$client->memories()->list(...);
$client->memories()->get(id);
$client->memories()->update(id, content?, importance?, tags?, metadata?);
$client->memories()->delete(id);
$client->memories()->bulkDelete(tenantId, projectId?);
$client->memories()->batch(memories); // Up to 100 per call
$client->memories()->supersede(id, content, metadata?);
$client->memories()->chain(id); // Get supersession chain
$client->memories()->promote(id, targetNamespace?, promotedBy?); // -> PromotionResult
$client->memories()->refresh(id); // Reset staleness -> HealthStatus
$client->memories()->feedback(tenantId, projectId, memoryIds); // -> ['updated' => int]Every write returns a Memory DTO with:
revision(int) -- read-your-writes token; pass torecall(waitForRevision:)embeddingStatus(string) -- embeddings are async:'pending'until indexedconflicts(array) -- on create, memories the new fact contradicts (empty unless contradiction detection is enabled)
$memory = $client->memories()->add(
content: 'User switched to PostgreSQL',
tenantId: 'user_123',
projectId: 'my-project',
);
// Read-your-writes: this recall is guaranteed to see the write above
$results = $client->memories()->recall(
query: 'What database does the user run?',
tenantId: 'user_123',
projectId: 'my-project',
waitForRevision: $memory->revision,
);$results = $client->memories()->recall(
query: 'deployment preferences',
tenantId: 'user_123',
projectId: 'my-project',
maxTokens: 2000, // token-budget recall
notTags: ['archived'], // exclude by tag
since: '2026-01-01T00:00:00Z', // time window
until: '2026-06-30T23:59:59Z',
usedIds: ['mem_01ABC'], // feedback from previous recall
);Report which recalled memories were actually useful -- they get a scoring boost on future recalls:
$result = $client->memories()->feedback(
tenantId: 'user_123',
projectId: 'my-project',
memoryIds: ['mem_01ABC', 'mem_02DEF'],
);
echo $result['updated']; // 2Or save a round trip by passing usedIds on the next recall() call.
$client->projects()->create(name, description?);
$client->projects()->list(limit?, offset?);
$client->projects()->get(id);
$client->projects()->delete(id);$client->webhooks()->create(url, events, secret?);
$client->webhooks()->list();
$client->webhooks()->delete(id);$client->bootstrap()->get(agentId, tenantId?, projectId?, maxTokens?, includeTypes?, excludeTypes?, recencyDays?);
$client->bootstrap()->configure(agentId, config);
$client->bootstrap()->showConfig(agentId);$client->health()->memory(id); // Single memory health status
$client->health()->refresh(id, note?); // Reset staleness score
$client->health()->namespace(tenantId?, projectId?); // Namespace-level health$client->audit()->list(filters?);
$client->audit()->export(format?, filters?); // CSV or JSON$client->erasure()->create(tenantId, reason?); // Erasure request
$client->erasure()->show(id);$client->export()->account(format?); // Full account export
$client->export()->namespace(tenantId, projectId?, format?);Query the entity graph built by the intelligence pipeline:
// List entities for a namespace (most-mentioned first)
$entities = $client->entities()->list(
tenantId: 'user_123',
projectId: 'my-project',
entityType: 'person', // optional filter
limit: 50, // optional (max 200)
);
foreach ($entities as $entity) {
echo "{$entity->name} ({$entity->type}): {$entity->memoryCount} memories\n";
}
// List memories mentioning an entity (metadata only, no content)
$result = $client->entities()->memories(
name: 'Jane Doe',
tenantId: 'user_123',
projectId: 'my-project',
);
// $result = ['entity' => 'Jane Doe', 'memories' => [...], 'total' => 3]PII entities appear under their stable IDs, never raw values.
$client->usage()->get();Memra is privacy-first. The PHP SDK provides access to data export and erasure endpoints.
// Export all account data
$data = $client->export()->account();
echo $data->exported_at;
// Export namespace data (per-tenant)
$data = $client->export()->namespace('tenant_123');
// Export namespace data filtered by project
$data = $client->export()->namespace('tenant_123', projectId: 'proj_1');// Request erasure of a memory
$request = $client->erasure()->create('mem_abc123');
echo $request->status; // 'pending'
// Check erasure status
$status = $client->erasure()->show('mem_abc123');
echo $status->status; // 'completed'Erasure is thorough: flat files, database index rows, Redis cache entries, and audit log entries are all purged. The erasure request is tracked with a scheduled deletion date and completion status.
| Type | Description |
|---|---|
fact |
Factual knowledge (e.g., "User works at Acme Corp") |
event |
Time-bound occurrences (e.g., "User deployed v2.0 on March 15") |
pattern |
Behavioral patterns (e.g., "User always reviews PRs before merging") |
working |
Short-term context, auto-expires after 24 hours |
decision |
Decisions with supersession chains (e.g., "Switched from MySQL to PostgreSQL") |
preference |
User preferences (e.g., "Prefers dark mode") |
context |
Contextual information (e.g., "Currently working on Project X") |
entity |
Entity references (e.g., "User's manager is Jane Doe") |
All API errors throw typed exceptions:
use Memra\Sdk\Exceptions\MemraAuthException;
use Memra\Sdk\Exceptions\MemraNotFoundException;
use Memra\Sdk\Exceptions\MemraValidationException;
use Memra\Sdk\Exceptions\MemraQuotaException;
use Memra\Sdk\Exceptions\MemraServerException;
use Memra\Sdk\Exceptions\MemraTimeoutException;
try {
$memory = Memra::add(
content: 'Important fact',
tenantId: 'user_123',
projectId: 'my-project',
);
} catch (MemraAuthException $e) {
// 401 -- invalid or missing API key
echo "Auth error: {$e->getMessage()}";
} catch (MemraValidationException $e) {
// 422 -- validation errors with per-field details
foreach ($e->errors as $field => $messages) {
echo "{$field}: " . implode(', ', $messages) . "\n";
}
} catch (MemraQuotaException $e) {
// 429 -- rate limited
echo "Rate limited. Retry after {$e->retryAfter} seconds.";
echo "Remaining: {$e->remaining}";
} catch (MemraNotFoundException $e) {
// 404 -- resource not found
echo "Not found: {$e->getMessage()}";
} catch (MemraServerException $e) {
// 500/502/503 -- server error
echo "Server error ({$e->statusCode}): {$e->getMessage()}";
} catch (MemraTimeoutException $e) {
// Connection timeout
echo "Request timed out: {$e->getMessage()}";
}All exceptions extend MemraException and include statusCode and responseBody properties.
Use Memra::fake() in your tests to avoid making real API calls:
use Memra\Sdk\Facades\Memra;
test('it stores a user preference', function () {
$fake = Memra::fake();
// Your application code that calls Memra::add()
storeUserPreference('user_123', 'dark mode');
// Assert the memory was stored
$fake->assertAdded();
// Assert with specific criteria
$fake->assertAdded(fn (array $args) =>
$args['content'] === 'User prefers dark mode'
&& $args['type'] === 'preference'
);
});
test('it recalls memories during conversation', function () {
$fake = Memra::fake();
getRelevantContext('user_123', 'What do I prefer?');
$fake->assertRecalled();
});
test('it handles empty state', function () {
$fake = Memra::fake();
// Nothing should happen in this test path
$fake->assertNothingSent();
});| Method | Description |
|---|---|
$fake->assertAdded(?callable) |
Assert add() was called, optionally matching a callback |
$fake->assertRecalled(?callable) |
Assert recall() was called, optionally matching a callback |
$fake->assertDeleted(?string $id) |
Assert delete() was called, optionally for a specific ID |
$fake->assertNothingSent() |
Assert no calls were made |
MIT License. See LICENSE for details.