API Documentation
Sav Account API v2 — the REST API that powers Notefox sync, accounts, and data management.
Contents
Overview
Base URL: https://notefox.eu/api/v2
The v2 API is a generic account platform called Sav Account. The account itself (signup, login, sessions, tokens, OTP, rate limiting) is shared and product-neutral. What is partitioned per service is only the synchronised data: every service owns its own snapshot and revision. Notefox is the first and default service.
- Every endpoint is POST with a JSON body, except
GET /status. Content-Type: application/json- Body limited to 2 MB; the
datafield to 1.5 MB (413beyond that). - Responses are always JSON with a real HTTP status.
Success response
{
"status": "Successful",
"code": 200,
"data": { ... }
}
Error response
{
"status": "Error",
"code": 402,
"description": "...",
"data": null
}
Authentication
Most endpoints require a login-id and token, both 64 hexadecimal characters,
obtained at login. Include them in the JSON body of every authenticated request.
Sessions can optionally have an expiry date. A session without expiry stays valid until an explicit logout or a password change.
Conventions
- Dates —
YYYY-MM-DD HH:MM:SSin the server timezone.GET /statusreturnsserver-timefor clock-skew measurement. - Verification codes — 6 characters from
23456789ABCDEFGHJKLMNPQRSTUVWXYZ(no ambiguous characters), compared case-insensitively. emailin the payload — The server only stores the SHA-512 hash. The address is used exclusively as the email recipient and only if the hash matches the account; otherwise410.servicein data endpoints — Optional, defaults tonotefox. Must match^[a-z0-9][a-z0-9_-]{0,31}$and be declared in the server registry.- Sync history — A permission, not a feature. The
data/get/history*endpoints requirehistory-enabled = 1on the account (default0, no endpoint to change it).
Error codes
| Code | HTTP | Meaning |
|---|---|---|
200 |
200 | OK |
201 |
200 | No data for this account |
400 |
400 | Missing or invalid parameters |
401 |
503 | Database unreachable |
402 |
401 | login-id missing, disabled, expired or invalid |
403 |
401 | Account not found |
404 |
401 | Token missing, disabled or expired |
405 |
401 | Invalid token |
406 |
405 | HTTP method not allowed |
407 |
413 | Payload too large |
409 |
409 | Revision conflict (response carries current data) |
410 |
401 | Invalid credentials |
411 |
403 | Account not active / not verified |
412 |
400 | Code expired |
413 |
400 | Invalid code |
414 |
409 | Account already verified |
415 |
400 | No code has been requested |
419 |
409 | Signup not completed |
420 |
429 | Too many wrong attempts, request a new code |
429 |
429 | Rate limit reached |
430 |
409 | Encryption key unavailable for this account |
431 |
409 | OTP is already in the requested state |
432 |
409 | Sync history not available for this service |
433 |
403 | Sync history not enabled for this account |
434 |
409 | History entry encrypted with a previous password |
452 |
429 | A deletion code has already been requested |
500 |
500 | Internal error (details in server log only) |
503 |
503 | Service temporarily unavailable |
Status
GET /status
Health check endpoint. No authentication required.
Response
{
"status": "Successful",
"code": 200,
"data": {
"reachable": true,
"database": true,
"schema": true,
"schema-details": {
"keys": true,
"snapshots": true,
"snapshots-multi-service": true,
"rate-limits": true,
"otp": true,
"otp-change-code": true,
"password-change-code": true,
"history-permission": true,
"pro-features": true,
"legacy-mirror": true
},
"mailer": true,
"services": ["notefox"],
"api-version": "2.0",
"server-time": "2024-05-01 10:00:00",
"server-timezone": "UTC"
}
}
- database boolean — Whether the DBMS is reachable.
- schema boolean — Whether the required DDL has been applied.
- schema-details object
— Per-block DDL status.
falsemeans that piece is missing. - mailer boolean —
Whether SMTP is available (no emails go out if
false). - services array — Services configured on this installation.
- server-time string — Current server time for clock-skew measurement.
Signup
POST /signup
Register a new account. The email verification code is always sent. The response is identical whether the address is free, already registered, or registered but never verified (prevents account enumeration).
Request body
{
"username": "Sara",
"password": "...",
"email": "sara@example.com"
}
- username string — Display name.
- password string — Account password (min 8 characters).
- email string — Email address (used as the identity root).
Response
{ "verification-required": true }
Errors
400 429
POST /signup/verify
Verify the signup email code. Activates the account, consumes the code, and creates the encryption key (DEK).
Request body
{
"email": "...",
"password": "...",
"verification-code": "A1B2C3"
}
- email string — The email used at signup.
- password string — The account password.
- verification-code string — The 6-character code received by email.
Response
{
"verified": true,
"username": "Sara",
"encryption-ready": true
}
Errors
410 414 412 413
420
POST /signup/verify/get-new-code
Resend the signup verification code. Rate limited.
Request body
{
"email": "...",
"password": "..."
}
- email string — The email used at signup.
- password string — The account password.
Response
{ "verification-required": true }
Errors
410 429
Login
POST /login
Authenticate with email and password. If OTP is enabled (default), a verification code is emailed and
must be confirmed via /login/verify. If OTP is disabled, the session is returned
directly.
Request body
{
"email": "...",
"password": "..."
}
- email string — Account email address.
- password string — Account password.
Response (OTP enabled)
{
"otp-required": true,
"login-id": "...",
"verification-expiry": "..."
}
Response (OTP disabled)
{
"otp-required": false,
"login-id": "...",
"token": "...",
"expiry": null,
"username": "Sara",
"encryption-ready": true
}
Errors
410 411 429
POST /login/verify
Complete login by providing the emailed verification code. The code is consumed and attempts are limited to 5.
Request body
{
"login-id": "...",
"email": "...",
"password": "...",
"verification-code": "A1B2C3"
}
- login-id string
— The
login-idreturned by/login. - email string — Account email address.
- password string — Account password.
- verification-code string — The 6-character code received by email.
Response
{
"login-id": "...",
"token": "...",
"expiry": null,
"username": "Sara",
"encryption-ready": true
}
Errors
410 412 413 420
POST /login/verify/get-new-code
Resend the login verification code. Rate limited.
Request body
{
"login-id": "...",
"email": "...",
"password": "..."
}
- login-id string
— The
login-idreturned by/login. - email string — Account email address.
- password string — Account password.
Response
{
"otp-required": true,
"login-id": "...",
"verification-expiry": "..."
}
POST /login/check-id
Check whether a session is still valid.
Request body
{
"login-id": "...",
"token": "..."
}
- login-id string — Session identifier (64 hex chars).
- token string — Session token (64 hex chars).
Response
{
"valid": true,
"username": "Sara",
"expiry": null,
"otp-enabled": true
}
Errors
402 404 405
POST /login/set-expiry
Set or remove the expiry date of a login session.
Request body
{
"login-id": "...",
"token": "...",
"expiry": "2025-01-01 00:00:00"
}
- expiry string|null
— Date string or
nullto remove the expiry.
Response
{
"login-id": "...",
"expiry": "...",
"old-expiry": null
}
POST /token/set-expiry
Set or remove the expiry date of the session token.
Request body
{
"login-id": "...",
"token": "...",
"expiry": "2025-01-01 00:00:00"
}
- expiry string|null
— Date string or
nullto remove the expiry.
Response
{
"expiry": "...",
"old-expiry": null
}
POST /logout
End the current session or all sessions. The token is mandatory (in v1, knowing only the login-id was enough).
Request body
{
"login-id": "...",
"token": "...",
"all-devices": false
}
- all-devices boolean
— If
true, invalidates all sessions for the account.
Response
{
"logged-out": true,
"all-devices": false
}
Two-factor authentication (OTP)
OTP is enabled by default on every account. This setting affects only the login: it never disables the email verification required by signup, nor the confirmation code required to change the password or delete the account.
POST /otp/status
Check whether OTP is enabled for the authenticated account.
Request body
{
"login-id": "...",
"token": "..."
}
Response
{ "otp-enabled": true }
POST /otp/enable
Enable OTP for the account. Requires the password and sends a notification email.
Request body
{
"login-id": "...",
"token": "...",
"password": "...",
"email": "..."
}
- password string — Account password.
- email string optional — Used only to send the notification email; validated against the account hash.
Response
{ "otp-enabled": true }
Errors
431 (already enabled)
POST /otp/disable
Request to disable OTP. Requires a valid token and the password, then emails a confirmation code.
Request body
{
"login-id": "...",
"token": "...",
"password": "...",
"email": "..."
}
- password string — Account password.
- email string — Used to send the confirmation code; validated against the account hash.
Response
{
"verification-required": true,
"verification-expiry": "..."
}
Errors
431 (already disabled) 503
POST /otp/disable/verify
Confirm OTP disable with the emailed code. Single-use, with expiry and max 5 attempts.
Request body
{
"login-id": "...",
"token": "...",
"password": "...",
"verification-code": "A1B2C3",
"email": "..."
}
- password string — Account password.
- verification-code string — The 6-character code received by email.
- email string optional — Used to send the confirmation notification.
Response
{ "otp-enabled": false }
Errors
412 413 420 503
Sync & Data
All sync endpoints accept the optional service field (default notefox) and echo
it back. An unknown or malformed service is refused with 400.
POST /data/insert
Write or update data for a service. Uses revision-based conflict detection.
Request body
{
"login-id": "...",
"token": "...",
"service": "notefox",
"data": "<json string>",
"updated-locally": "2024-05-01 10:00:00",
"base-revision": 41
}
- service string optional, default "notefox" — Target service.
- data string — JSON data to store (max 1.5 MB).
- updated-locally string — Client-side timestamp. Must be a valid date, not in the future.
- base-revision integer optional — The revision the client currently owns. Without it the write is always accepted (last-write-wins).
Success response
{
"service": "notefox",
"revision": 42,
"updated-server": "2024-05-01 10:00:02",
"updated-locally": "2024-05-01 10:00:00"
}
Conflict response (HTTP 409)
{
"status": "Error",
"code": 409,
"description": "Revision conflict",
"data": {
"service": "notefox",
"revision": 47,
"updated-server": "...",
"updated-locally": "...",
"data": "<current json>"
}
}
Errors
400 407 409 430
POST /data/get
Get the current data snapshot for a service.
Request body
{
"login-id": "...",
"token": "...",
"service": "notefox"
}
- service string optional, default "notefox" — Target service.
Response
{
"service": "notefox",
"data": "<json string>",
"revision": 42,
"updated-locally": "...",
"updated-server": "..."
}
Errors
201 (no data) 430
POST /data/get/last-update
Get the revision and timestamps without decrypting the data. Token mandatory.
Request body
{
"login-id": "...",
"token": "...",
"service": "notefox"
}
- service string optional, default "notefox" — Target service.
Response
{
"service": "notefox",
"revision": 42,
"updated-locally": "...",
"updated-server": "..."
}
POST /data/services
List all services that hold data for the authenticated account.
Request body
{
"login-id": "...",
"token": "..."
}
Response
{
"services": [
{
"service": "notefox",
"revision": 42,
"updated-server": "...",
"updated-locally": "..."
}
],
"supported": ["notefox"],
"history-enabled": false,
"pro-features": false
}
- services array — Services that have stored data for this account.
- supported array — All services configured on this installation.
- history-enabled boolean — Whether sync history is enabled for this account.
- pro-features boolean — Whether premium features are enabled for this account.
POST /data/get/history
List past synced versions of a service (newest first, up to 30 entries). Requires history-enabled
permission on the account.
Request body
{
"login-id": "...",
"token": "...",
"service": "notefox"
}
- service string optional, default "notefox" — Target service.
Response
{
"service": "notefox",
"entries": [
{
"id": 512,
"inserted-date": "2024-05-01 10:00:02",
"updated-locally-date": "2024-05-01 10:00:00"
}
]
}
Errors
432 433
POST /data/get/history/download
Download a specific history entry by ID. The server decrypts before responding. Requires history-enabled
permission.
Request body
{
"login-id": "...",
"token": "...",
"service": "notefox",
"id": 498
}
- id integer — One
of the IDs returned by
/data/get/history.
Response
{
"service": "notefox",
"id": 498,
"inserted-date": "2024-04-28 09:12:44",
"updated-locally-date": "2024-04-28 09:12:40",
"data": "<plaintext json>"
}
Errors
201 (not found) 432 433 434
Account management
Changing the password and deleting the account are critical operations: both always require the emailed confirmation code, regardless of the OTP setting.
POST /password/edit
Request a password change. Emails a confirmation code. Nothing is written until the code is verified.
Request body
{
"login-id": "...",
"token": "...",
"password": "...",
"new-password": "...",
"email": "..."
}
- new-password string optional here — At least 8 characters, different from current. If sent, rules are checked immediately (no email sent for an invalid change).
Response
{
"verification-required": true,
"verification-expiry": "..."
}
POST /password/edit/get-new-code
Resend the password change confirmation code. Rate limited (3 per 15 minutes).
Request body
{
"login-id": "...",
"token": "...",
"password": "...",
"email": "..."
}
- password string — Current password.
- email string — Account email address.
Response
{
"verification-required": true,
"verification-expiry": "..."
}
Errors
415 (no change requested) 429
POST /password/edit/verify
Complete the password change. Only the encryption key (DEK) is re-wrapped — no note is re-encrypted. All other sessions are invalidated and a new session is returned.
Request body
{
"login-id": "...",
"token": "...",
"password": "...",
"new-password": "...",
"verification-code": "A1B2C3",
"email": "..."
}
- password string — Current password.
- new-password string — New password (at least 8 characters, different from current).
- verification-code string — Code received by email.
- email string — Account email address.
Response
{
"login-id": "<new>",
"token": "<new>",
"expiry": null
}
Errors
412 413 420
POST /delete
Request account deletion. Emails a confirmation code.
Request body
{
"email": "...",
"password": "..."
}
- email string — Account email address.
- password string — Current password.
Response
{
"verification-required": true,
"verification-expiry": "..."
}
Errors
452 (code already requested)
POST /delete/verify
Confirm account deletion. Permanently removes the account, all sessions, all data snapshots, encryption keys, and legacy data in one transaction.
Request body
{
"email": "...",
"password": "...",
"deleting-code": "A1B2C3"
}
- email string — Account email address.
- password string — Current password.
- deleting-code string — Confirmation code received by email.
Response
{ "deleted": true }
Errors
410 412 413 420
POST /delete/verify/get-new-code
Resend the account deletion confirmation code.
Request body
{
"email": "...",
"password": "..."
}
- email string — Account email address.
- password string — Current password.
Response
{
"verification-required": true,
"verification-expiry": "..."
}
Diagnostics
POST /error-logs/insert
Submit client error logs. No authentication required. Rate limited per IP. Length limits on every field.
Request body
{
"datetime": "2024-05-01T10:00:00Z",
"context": "popup.js:handleSync",
"error": "TypeError: Cannot read property ...",
"url": "https://example.com",
"notefox-version": "4.6.0",
"anonymous-userid": "a1b2c3..."
}
- datetime string — Client date/time when the error occurred.
- context string max 500 — Where the error happened (file, function, component).
- error string max 65535 — Error message or stack trace.
- url string optional, max 1000 — URL of the page where the error occurred.
- notefox-version string optional, max 20 — Extension version.
- anonymous-userid string optional, max 50 — Anonymous client identifier.
Response
{ "data": null }
POST /telemetry/insert
Submit client telemetry data. No authentication required. Rate limited per IP. Length limits on every field.
Request body
{
"notefox-account": true,
"anonymous-userid": "a1b2c3...",
"client-datetime": "2024-05-01T10:00:00Z",
"language": "en",
"action": "sync",
"context": "popup",
"url": "https://example.com",
"browser": "firefox",
"browser-version": "126.0",
"notefox-version": "4.6.0",
"os": "macos",
"other": ""
}
- notefox-account boolean — Whether the user is signed in to a Notefox account.
- anonymous-userid string max 50 — Anonymous client identifier.
- client-datetime string — Client date/time of the event.
- language string max 20 — Client language code.
- action string max 500 — Action being tracked (e.g. "sync", "open-popup").
- context string optional, max 500 — Additional context for the action.
- url string optional, max 65535 — Page URL when the event fired.
- browser string max 20 — Browser name.
- browser-version string optional, max 20 — Browser version.
- notefox-version string max 20 — Extension version.
- os string optional, max 20 — Operating system.
- other string optional, max 65535 — Any additional data.
Response
{ "data": null }
Rate limits
Rate limits protect the shared platform. Counters are per account, not per service.
Beyond the limit: 429 with a temporary block.
| Bucket | Subject | Limit |
|---|---|---|
| login | 10 / 15 min | |
| login-ip | IP | 30 / 15 min |
| otp-verify | login-id | 5 / 15 min |
| otp-resend | email or login-id | 3 / 15 min |
| signup-ip / signup-email | IP / email | 10 and 5 per hour |
| signup-verify | 10 / 15 min | |
| otp-change / otp-change-verify | user | 5–10 / 15 min |
| password-edit / password-edit-verify | user | 5 / 15 min |
| delete-request / delete-verify | 3 per hour / 5 per 15 min | |
| data-insert | user | 120 / min |
| data-services | user | 60 / min |
| data-history | user | 60 / min |
| data-history-download | user | 30 / min |
| error-logs / telemetry | IP | 30 / 10 min |
Encryption model
All user data is encrypted at rest. The server never stores the plaintext email (only its SHA-512 hash) or the plaintext password.
password ──PBKDF2(sha256, 210k, salt)──> KEK ──AES-256-CBC──> wrapped DEK (user_keys)
DEK ──AES-256-CBC──> data, snapshot of every service (sav_data_current)
token ──AES-256-CBC──> password (tokens, v1 format)
- The DEK (Data Encryption Key) is generated once with
random_bytes(32)and never changes. It encrypts all data across all services. - The password only ever encrypts the DEK. Changing the password re-wraps ~100 bytes — no note is re-encrypted.
key-checkis the SHA-512 fingerprint of the DEK: it proves an unwrap produced the right key without storing the key.- The
tokenstable stores the password encrypted with the token (v1 compatibility), so a v2-issued token works on v1 endpoints.