Copy your memories to storage you control, on a schedule
You can already export everything at any time by asking your assistant. This page is for the other case: an unattended job (a weekly cron, a scheduled task, a CI workflow) that copies your memories somewhere you control without an AI session running.
Ask your AI assistant to create one:
Create an Amber backup key for me
It calls amber_create_backup_key and shows you a key beginning with
amber_bk_. It is shown once. Amber stores only a
hash of it, so it cannot be recovered, only replaced. Save it somewhere you keep
secrets.
First save the key to a file. The scheduled job below reads it from there, and it keeps the key out of your shell history:
mkdir -p ~/.amber && umask 077
printf '%s' 'PASTE_YOUR_KEY_HERE' > ~/.amber/backup-key
chmod 600 ~/.amber/backup-key
A backup is a task, not a single download. You start it, poll until it
says complete, then fetch the file:
# start it -- returns immediately with a task id
curl -fsS -X POST -H "X-Amber-Backup-Key: $(cat ~/.amber/backup-key)" \
https://next-api.ambermem.com/backup
# check on it
curl -fsS -H "X-Amber-Backup-Key: $(cat ~/.amber/backup-key)" \
https://next-api.ambermem.com/backup/PASTE_THE_TASK_ID
Once status is complete the reply carries a
download_url, signed and good for 7 days, which is long enough that an
interrupted download can be resumed rather than restarted. Treat it as a password: anyone
holding it can download your memories until it expires, and it cannot be revoked. Poll
again any time for a fresh one.
The download supports resuming. If a transfer drops, curl -C - against the
same URL continues from where it stopped instead of starting again.
Use a small script rather than a one-line cron entry. Two reasons, and both bite in
practice: a shell variable assigned on the same line as the command is expanded
before the assignment applies, so the key would be sent empty; and cron treats
% as a newline, which truncates any command containing a date format.
~/bin/amber-backup.sh (chmod +x ~/bin/amber-backup.sh)
#!/usr/bin/env bash
set -euo pipefail
KEY="$(cat "$HOME/.amber/backup-key")"
API="https://next-api.ambermem.com"
DEST="$HOME/backups/amber"
mkdir -p "$DEST"
OUT="$DEST/amber-$(date +%F).json.gz"
# 1. Start the task. -f makes an HTTP error a non-zero exit instead of a saved error page.
START="$(curl -fsS --retry 3 --retry-delay 30 -X POST \
-H "X-Amber-Backup-Key: $KEY" "$API/backup")"
TASK="$(printf '%s' "$START" | grep -o '"task_id":"[^"]*"' | cut -d'"' -f4)"
[ -n "$TASK" ] || { echo "ERROR: no task id in: $START" >&2; exit 1; }
# (with jq installed: TASK=$(printf '%s' "$START" | jq -r .task_id) )
# 2. Wait for it. A large account takes several background passes.
STATUS=''
for _ in $(seq 1 120); do
STATUS="$(curl -fsS -H "X-Amber-Backup-Key: $KEY" "$API/backup/$TASK")"
case "$STATUS" in
*'"status":"complete"'*) break ;;
*'"status":"failed"'*) echo "ERROR: backup failed: $STATUS" >&2; exit 1 ;;
esac
sleep 5
done
URL="$(printf '%s' "$STATUS" | grep -o '"download_url":"[^"]*"' | cut -d'"' -f4)"
[ -n "$URL" ] || { echo 'ERROR: timed out waiting for the backup' >&2; exit 1; }
# 3. Download it.
curl -fsSL --retry 3 --retry-delay 10 --max-time 900 "$URL" -o "$OUT.part"
# Verify the archive before keeping it. A half-written file is the one failure a backup cannot
# afford, and it takes TWO checks, not one. gzip catches corruption -- but the archive is a
# series of gzip members, so a file cut exactly at a member boundary is still structurally
# valid gzip and passes -t while the JSON inside it is missing its end.
if ! gzip -t "$OUT.part"; then
echo 'ERROR: downloaded archive is corrupt - not saved' >&2
rm -f "$OUT.part"
exit 1
fi
# So check the document really ends. The download itself declares its length, which is what
# makes curl above fail rather than save a short file; this catches a truncation that happened
# anywhere else -- a full disk while copying it, a sync that stopped halfway.
if [ "$(gzip -dc "$OUT.part" | tail -c 2)" != ']}' ]; then
echo 'ERROR: archive is incomplete (no closing marker) - not saved' >&2
rm -f "$OUT.part"
exit 1
fi
mv "$OUT.part" "$OUT"
echo "OK: $OUT"
Then schedule it for 04:00 every Sunday (crontab -e):
0 4 * * 0 /home/YOU/bin/amber-backup.sh >> /home/YOU/backups/amber/backup.log 2>&1
Download first, then upload. Do not pipe curl straight into aws:
a pipeline reports the last command's exit status, so a download that dies halfway
uploads a truncated file and the job still reports success, which is exactly what
-f was there to prevent.
# ...after the integrity check above, in the same script:
aws s3 cp "$OUT" "s3://your-bucket/amber/$(date +%F).json.gz"
%USERPROFILE%\bin\amber-backup.ps1, registered with Task Scheduler:
$ErrorActionPreference = 'Stop'
$key = (Get-Content "$HOME\.amber\backup-key" -Raw).Trim()
$api = 'https://next-api.ambermem.com'
$dest = "$HOME\backups\amber"
New-Item -ItemType Directory -Force -Path $dest | Out-Null
$out = Join-Path $dest ("amber-{0}.json.gz" -f (Get-Date -Format yyyy-MM-dd))
$hdr = @{ 'X-Amber-Backup-Key' = $key }
# 1. Start the task.
$task = (Invoke-RestMethod -Method Post -Uri "$api/backup" -Headers $hdr).task_id
# 2. Wait for it.
$status = $null
foreach ($i in 1..120) {
$status = Invoke-RestMethod -Uri "$api/backup/$task" -Headers $hdr
if ($status.status -eq 'complete') { break }
if ($status.status -eq 'failed') { throw "Backup failed: $($status.error)" }
Start-Sleep -Seconds 5
}
if ($status.status -ne 'complete') { throw 'Timed out waiting for the backup' }
# 3. Download it.
Invoke-WebRequest -Uri $status.download_url -OutFile "$out.part"
# A zero-length archive means the download never completed.
if ((Get-Item "$out.part").Length -eq 0) {
Remove-Item "$out.part"
throw 'Downloaded archive is empty - not saved'
}
Move-Item "$out.part" $out -Force
A JSON file: every memory with its content, subjects, topics, metadata and dates, plus anything still in the trash. Deletion is reversible for 30 days, so a backup that dropped it would destroy something you can still restore.
{
"exported_at": "2026-08-28T04:00:00.000Z",
"memories": [
{
"id": "...",
"content": "...",
"subjects": ["..."],
"topics": ["..."],
"metadata": {},
"created_at": "..."
}
]
}
The download declares its size, so a connection that dies halfway makes curl
fail rather than quietly leave you a shorter file. That matters more than it sounds: the
archive is a series of gzip members, so a file cut at exactly the wrong place is still
valid gzip, and gzip -t would pass it. The script above therefore also
checks the document ends.
The file is gzipped. Read it with gunzip amber-backup.json.gz, or inspect
it in place with zcat / gzcat. Compressed, a memory is roughly
a tenth of its JSON size.
| Endpoint | Status | Meaning |
|---|---|---|
POST /backup | 202 | Started. The body carries task_id and status_url |
POST /backup | 429 | Too soon. One backup per 60 minutes; honour Retry-After |
GET /backup/<id> | 200 | status is pending, running, complete or failed. Polling is free and never counts against the rate limit |
GET /backup/<id> | 404 | No such task, or it is older than 7 days |
| either | 401 | Missing, unknown, or revoked key |
| either | 500 | Something failed on our side. Retry, because a failed start does not consume your rate limit |
A finished backup and its file are kept for 7 days, then deleted. Take the copy you want to keep; the one on our side is a handover, not an archive.
No, and there is no size limit. The export is built in slices and compressed as it
goes, so nothing is bounded by what fits in one request or in memory. Earlier versions
stopped at 100,000 memories and set a truncated flag; both are gone.
If a slice cannot be completed, the task goes to failed with a reason
rather than sitting on running forever, so the script above exits
non-zero and writes no file. A partial backup you trust is worse than an obvious
failure, and a task that never finishes is worse than either, because nothing ever
tells you to look.
No. A backup key does not expire and has no TTL. It works until you replace or revoke it. That is deliberate: a key that quietly stopped working would leave a weekly job failing silently, which is the failure a backup can least afford.
A backup that stops does not announce itself: the job gets an error, writes it to a log nobody reads, and you find out on the day you need it. So Amber watches two things. If something presents a key you replaced or revoked, it says so on your next conversation with your assistant, naming the date you replaced it. And if no backup has succeeded for twice as long as you said they run, it says that too, whatever the cause: a dead key, a machine that is off, a disabled schedule, a failing script.
You decide where that second line sits. When you create the key, tell your assistant how
long a gap should count as a problem and it passes warn_after_days. The number
is used exactly as given: 10 means you hear about it once nothing has succeeded for ten
days. Leave room for a missed run, so a nightly job is comfortable around 3 and the weekly
script above around 14. Without a number Amber waits 30 days, which is a long time if you
back up every night. The warning appears at most once a day and stops on its own as soon
as a backup succeeds.
The simplest restore is one you never upload at all. If your backup already lives somewhere reachable over HTTPS (S3, R2, Backblaze, Google Cloud Storage, your own MinIO), just give Amber the link:
curl -fsS -X POST -H "X-Amber-Backup-Key: $(cat ~/.amber/backup-key)" -H "Content-Type: application/json" -d '{"url":"https://your-bucket.example/amber-backup.json.gz"}' https://next-api.ambermem.com/import/from-url
You can also just ask your assistant to do it. amber_import_memories
takes the same URL.
Amber never copies the file. It reads the archive from your URL a piece at a time as it works, and keeps nothing. Two consequences worth knowing: the link has to stay valid until the restore finishes, which can take hours on a large account, so use one good for at least 30 days. The host also has to support range requests, which every major object store does.
Then there is nothing for Amber to read, so it has to be uploaded. The easy way is the restore page. Ask your assistant to restore a local file and it hands you a link that opens it ready to go, with no key to paste: the link carries a one-off upload permission rather than a credential, so whatever key your backup job holds is left alone. Drop the file in, then tell your assistant, and it starts the restore. The same uploader also appears inside the chat if your client renders panels. Opening the page directly still works: it asks for your backup key instead.
By hand, it is two calls. Ask for an upload link and PUT the file straight to storage, and it never passes through Amber:
KEY="$(cat ~/.amber/backup-key)"
UP="$(curl -fsS -X POST -H "X-Amber-Backup-Key: $KEY" https://next-api.ambermem.com/import/presign)"
# The reply carries upload_url and complete_url.
curl -fsS -X PUT --upload-file amber-backup.json.gz "PASTE_upload_url"
curl -fsS -X POST -H "X-Amber-Backup-Key: $KEY" "PASTE_complete_url"
The upload link is good for 7 days, so an interrupted upload can be retried rather than restarted.
Either way, a restore is a task: the reply carries a
task_id and a status_url you poll until it says
complete.
It cannot duplicate your memories. Every memory keeps the id it was exported under, and a restore skips any id already present, so running it twice, or restoring a backup that overlaps what is already there, changes nothing the second time. A restore that stops part-way resumes instead of starting over. Trash comes back as trash, not as live memories.
Anything already in your account is left alone. A restore adds what is missing; it is not a rollback, and there is no "replace everything" mode, because that would be a way to lose memories, and nothing here is allowed to do that.
Ask your assistant to revoke it (amber_revoke_backup_key), or simply create
a new one. There is one key per account, so creating replaces the old one and the
old one stops working immediately.
A backup key can do exactly two things: download an export, and restore one. It cannot read individual memories, browse your account, or touch your subscription.
Restoring is more than adding, though, and it is worth being straight about it. An archive records which memories were in the trash, and a restore puts those back as trash so that deleting something is not quietly undone. That means somebody holding your key could restore a doctored archive and move memories you still have into the trash. Nothing is destroyed and the trash keeps everything for 30 days, so it is recoverable, but it is a change rather than only an addition. Revoke a key you no longer trust; it takes one step.
Because that would mean Amber storing your cloud credentials. That is a much larger thing to lose than a download key, and it would contradict the point of the product: we collect as little as possible about you. Pulling keeps your credentials yours.