OmegaSafe API is currently available as OmegaSafe Cloud. OmegaSafe Self-Hosted is planned but not available yet. Both OmegaSafe Cloud and the planned OmegaSafe Self-Hosted are OmegaSafe API server flavours, and the intention is to keep interface parity so programmatic integrations can switch between them without changing the stable JSON/API contract.
The OmegaSafe API was not adjusted to allow integrations. It was built with that intention from the start. Clear spec, best practices and documentation are a top priority.
This page documents the public OmegaSafe API surface currently available in OmegaSafe Cloud for programmatic integrations. It focuses on the stable JSON/API contract exposed to Bearer-token and API-key clients.
Browser and session mechanics used by first-party web forms are not part of the stable automation contract and are documented separately where relevant.
It excludes admin-only and non-public operational internals, plus browser-only management flows that are not intended for Bearer-token or API-key automation.
1. Base URL#
https://app.omega-safe.comThe shown base URL is the OmegaSafe Cloud API base. During interactive setup, OmegaSafe CLI — init and OmegaSafe CLI — create-http-ecs use OmegaSafe Cloud as the default suggested HTTP ECS connection target.
All API endpoints are served over HTTPS. HTTP is not supported.
2. API Conventions#
UUID Field Naming#
All request and response resource identifiers use specific, descriptive UUID field names to avoid ambiguity.
| Resource | Parameter Name | Example |
|---|---|---|
| Key | key_uuid | 123e4567-e89b-12d3-a456-426614174000 |
| API key | api_key_uuid | 0f3a9c4d-e89b-12d3-a456-426614174010 |
| Storage | storage_uuid | 234e5678-e89b-12d3-a456-426614174001 |
| Delta | delta_uuid | 345e6789-e89b-12d3-a456-426614174002 |
| Download release | download_release_uuid | 389e0123-e89b-12d3-a456-426614174011 |
| User | user_uuid | 456e7890-e89b-12d3-a456-426614174003 |
| Sale | sale_uuid | 567e8901-e89b-12d3-a456-426614174004 |
All UUIDs follow the standard RFC 4122 format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.
UUID format validation failures return 422 Unprocessable Entity with a field-specific message embedded in error_description, for example {'key_uuid': ['Invalid key UUID format.']}.
HTTP Method Patterns#
| Method | Usage |
|---|---|
| GET | Retrieve resources or route-specific representations |
| POST | Create resources, perform actions, and handle body-based lookups/queries |
| PATCH | Partial update on routes that explicitly support it |
| DELETE | Remove resources |
PUT is not currently used in this API.
Body-based lookups use POST with the UUID in the request body (not query parameters) to avoid logging sensitive identifiers in server access logs or browser history. For example: POST /keys/by_uuid, POST /storages/by_uuid.
Request Header Conventions#
For JSON requests and to force JSON responses on hybrid routes:
Content-Type: application/json
Accept: application/jsonResponse Negotiation Rules#
The API uses the Accept header to choose JSON or HTML behavior on hybrid routes:
- Missing
Acceptheader → JSON preferred Accept: application/json→ JSON preferredAccept: text/html→ HTML preferredAccept: */*→ HTML-preferred- Mixed values follow preference (for example
Accept: application/json, */*resolves to JSON)
For API clients using curl against hybrid endpoints, always send Accept: application/json. curl otherwise sends Accept: */*, which is HTML-preferred in this application.
Date and Time Formats#
Fields ending in _ts use Unix epoch timestamps (seconds since 1970-01-01 00:00:00 UTC). Every _ts field has a paired _datetime companion field in ISO 8601 UTC format with an explicit +00:00 offset. Shared serializers add these companion fields automatically.
{
"exp_ts": 1704153600,
"exp_datetime": "2025-01-01T00:00:00+00:00"
}For client-side rendering, exp_ts is usually the simpler field to pass into date and time libraries, because most of them already support Unix-timestamp conversion and formatting directly.
Status Code Patterns#
| Operation | Status Code |
|---|---|
| Create (POST) | 201 Created |
| Read (GET) | 200 OK |
| Update (PATCH) | 200 OK |
| Delete (DELETE) | 200 OK |
3. Authentication#
The OmegaSafe API supports three general authentication methods with different use cases and priorities.
Authentication Priority#
When multiple authentication methods are provided, the server uses this priority order:
Authorization: Bearer <token>(highest priority)X-Api-Key: <key>Cookie: auth_token=<token>(lowest priority, browser fallback)
Bearer Session Tokens#
Use POST /users/login to obtain a session token. The token is opaque and backed by a server-side session record. It is not a JWT and must not be parsed or decoded by clients.
curl -X POST https://app.omega-safe.com/users/login \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"email": "user@example.com",
"password": "SecurePass123!"
}'Use the token in the Authorization header:
curl -X GET https://app.omega-safe.com/keys/list \
-H "Accept: application/json" \
-H "Authorization: Bearer AUTH_SESSION_TOKEN"Session renewal uses a sliding-window model:
- Renewal fires only on
GETandHEADrequests.POST,PATCH, andDELETErequests do not trigger renewal. - When the remaining TTL of a session falls at or below the renewal threshold (default 900 seconds / 15 minutes), the session expiry is extended.
- The new expiry is set to
now + 1 hour, capped at an absolute maximum of 24 hours from the original session creation time. - A user active every 15 minutes gets a rolling 1-hour window, hard-capped at 24 hours from the original session creation.
- There is no separate refresh endpoint. Bearer-token clients must log in again after expiry or revocation.
Session revocation events — the following events revoke all active sessions for the affected user:
- Logout (
GET /users/logout) — revokes the current request-scoped session. - Password change, email change verification, account deactivation, or MFA disable in the account UI — revoke all active sessions for the user.
API Keys#
API keys provide long-lived authentication for programmatic access. Each user can create up to 10 named API keys with specific scope-based permissions. Create and manage them in OmegaSafe Cloud under My Account > API Keys. Those browser-only management flows are intentionally excluded from this reference. Include the key in the X-Api-Key header:
curl -X GET https://app.omega-safe.com/keys/list \
-H "Accept: application/json" \
-H "X-Api-Key: YOUR_API_KEY"The plaintext token is shown only once at creation or refresh time. Store it securely.
API Key Scope Table#
API keys require specific scopes to access routes. Select only the scopes needed for your use case (principle of least privilege).
| Scope | Description | Example routes |
|---|---|---|
keys:read | Read key metadata and delta info | GET /keys/list, POST /keys/by_uuid, POST /keys/internal_storage_limits, POST /deltas/list, POST /deltas/download_url |
keys:write | Create keys, upload deltas, manage storage | POST /keys/register, PATCH /keys/edit, PATCH /keys/claim_alpha, POST /deltas/upload_url, POST /deltas/confirm_upload, PATCH /deltas/claim |
keys:delete | Delete keys and deltas | DELETE /keys/delete, DELETE /deltas/delete |
storage:read | List and view storage configurations | GET /storages/list, POST /storages/by_uuid |
storage:write | Add and edit storage configurations | GET /storages/bucket_add, POST /storages/bucket_add, POST /storages/bucket_edit, PATCH /storages/bucket_edit |
storage:delete | Delete storage configurations | DELETE /storages/delete |
user:read | Read user account data | GET /users/me |
software:read | Access software download routes | POST /download/cli/latest_version, POST /download/cli/download, POST /download/server/latest_version, POST /download/server/download |
invitations:read | List key invitations | GET /keys/invitations/list |
GET /users/connection_test accepts any valid API key and does not require a scope.
PATCH /keys/share, POST /keys/invitation_accept, and DELETE /keys/invitation_delete do not allow API key authentication.
Browser Session Cookie#
Browser sessions can also authenticate with the auth_token cookie. Cookie issuance, renewal, and related browser-session mechanics are intentionally out of scope for the stable public automation contract.
4. Users API#
POST /users/register#
Create a new account.
Auth: None | Role: None | API key: N/A | Mode: Hybrid (HTML + JSON)
Request body:
{
"email": "user@example.com",
"password": "SecurePass123!",
"confirm_password": "SecurePass123!",
"terms_accepted": true,
"product_updates_opt_in": true,
"altcha": "ALTCHA_PAYLOAD"
}Fields:
email— required email address.password— required. Must be 10–1000 characters and contain at least one letter, one number, and one special character.confirm_password— required. Must satisfy the same password rules and matchpassword.terms_accepted— required boolean and must betrue.product_updates_opt_in— optional boolean, defaults tofalse.altcha— required string. See Help — When is ALTCHA required?.
Response (201 Created):
{
"message": "Thank you for registering. If the request is eligible, check your email to complete the process. If you do not receive it, wait and try registering again later."
}Behavior notes:
- The route uses the same outward response for a brand new email, an existing inactive account, and an existing active account. This prevents an unauthenticated client from detecting whether an account exists for a given email.
- For a brand new email and for an existing inactive account, the route attempts to send an activation email. Delivery is subject to internal abuse-prevention limits.
- Re-registering an existing inactive account overwrites the stored password hash with the new password and generates a new activation token only when the resend is allowed by the internal email-throttle policy.
- Re-registering an existing active account attempts to send a warning email about the registration attempt.
- A brand new account starts inactive and receives trial billing defaults.
- The JSON response does not include
user_uuid.
Errors:
400 Bad Request— ALTCHA payload passed schema validation but failed server-side parsing or verification422 Unprocessable Entity— schema validation failed, including invalid email, weak password, password mismatch, missing ALTCHA, orterms_accepted != true
curl -X POST https://app.omega-safe.com/users/register \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"email": "newuser@example.com",
"password": "SecurePass123!",
"confirm_password": "SecurePass123!",
"terms_accepted": true,
"altcha": "ALTCHA_PAYLOAD"
}'GET /users/activate#
Activate a user account using the token from the activation email.
Auth: None | Role: None | API key: N/A | Mode: Hybrid (HTML redirect + JSON)
Query parameters:
token— required activation token.
Response (200 OK):
{
"message": "Your account has been activated."
}Errors:
404 Not Found— activation token is invalid or expired (JSON clients)422 Unprocessable Entity— missing query token
curl -X GET "https://app.omega-safe.com/users/activate?token=ACTIVATION_TOKEN" \
-H "Accept: application/json"POST /users/login#
Authenticate a user.
Auth: None | Role: None | API key: N/A | Mode: Hybrid (HTML + JSON)
Request body:
{
"email": "user@example.com",
"password": "SecurePass123!",
"altcha": "ALTCHA_PAYLOAD"
}Fields:
email— required email address.password— required password.altcha— optional until login protection requires a challenge; then it becomes mandatory. See Help — When is ALTCHA required? and OmegaSafe API — Browser & Interactive Flows (Advanced / Non-API Automation).
Success response (200 OK):
{
"message": "Login successful.",
"user_uuid": "123e4567-e89b-12d3-a456-426614174000",
"token": "AUTH_SESSION_TOKEN",
"exp_ts": 1773655200,
"exp_datetime": "2026-03-16T10:00:00+00:00",
"billing_slots": 5,
"billing_exp_ts": 1775001600,
"billing_exp_datetime": "2026-04-01T00:00:00+00:00"
}MFA challenge response (202 Accepted): See OmegaSafe API — Browser & Interactive Flows (Advanced / Non-API Automation).
Errors:
400 Bad Request— ALTCHA payload passed schema validation but failed server-side verification401 Unauthorized— wrong credentials or inactive account422 Unprocessable Entity— validation failure
POST /users/login/mfa#
Complete MFA verification after the initial login challenge.
Auth: None | Role: None | API key: N/A | Mode: Hybrid (HTML form + JSON)
Request body:
{
"mfa_token": "MFA_CHALLENGE_TOKEN",
"code": "123456",
"remember_device": false
}Fields:
mfa_token— required MFA challenge token fromPOST /users/login.code— required 6-digit TOTP code.remember_device— optional boolean, defaults tofalse.
Success response (200 OK):
{
"message": "Login successful.",
"user_uuid": "123e4567-e89b-12d3-a456-426614174000",
"token": "AUTH_SESSION_TOKEN",
"exp_ts": 1773655200,
"exp_datetime": "2026-03-16T10:00:00+00:00",
"billing_slots": 5,
"billing_exp_ts": 1775001600,
"billing_exp_datetime": "2026-04-01T00:00:00+00:00"
}Errors:
401 Unauthorized— expired challenge, MFA disabled for the account, or incorrect code422 Unprocessable Entity— validation failure
GET /users/logout#
End the current session.
Auth: Required | Role: User | API key: No | Mode: Hybrid (HTML redirect + JSON)
Response (200 OK):
{
"message": "Logout successful."
}Behavior notes:
- Revokes the current server-side auth session.
- If the request was authenticated with
Authorization: Bearer ..., that Bearer token stops working immediately after logout completes.
curl -X GET https://app.omega-safe.com/users/logout \
-H "Accept: application/json" \
-H "Authorization: Bearer YOUR_TOKEN"GET /users/me#
Return the authenticated user’s account payload.
Auth: Required | Role: User | API key: Allowed (requires user:read scope) | Mode: Hybrid (HTML + JSON)
Response (200 OK):
{
"user_uuid": "123e4567-e89b-12d3-a456-426614174000",
"email": "user@example.com",
"country_evidence_json": {
"customer_country": {
"country": "GB",
"source": "billing_profile"
},
"ip_country": {
"country": "GB",
"source": "ipinfo.io"
},
"collected_at_ts": 1773568800,
"collected_at_datetime": "2026-03-15T10:00:00+00:00"
},
"app_role_id": 3,
"app_role_name": "User",
"activated": true,
"terms_and_privacy_policy_accepted": true,
"terms_and_privacy_policy_accepted_ts": 1773568800,
"terms_and_privacy_policy_accepted_datetime": "2026-03-15T10:00:00+00:00",
"product_updates_opt_in": true,
"api_exp_email_opt_in": false,
"key_removal_email_opt_in": true,
"keys": [
{
"key_uuid": "234e5678-e89b-12d3-a456-426614174001",
"name": "Team Key",
"role_id": 1,
"role_name": "Owner",
"storage_uuid": "345e6789-e89b-12d3-a456-426614174002",
"storage_name": "Primary R2"
}
],
"owned_storages": [
{
"storage_uuid": "345e6789-e89b-12d3-a456-426614174002",
"name": "Primary R2"
}
]
}JSON fields:
user_uuidemailcountry_evidence_json—nullwhen no country evidence snapshot has been stored; otherwise the decrypted JSON value currently stored on the user record, typically containingcustomer_country.country,customer_country.source,ip_country.country,ip_country.source,collected_at_ts,collected_at_datetimeapp_role_idapp_role_nameactivatedterms_and_privacy_policy_acceptedterms_and_privacy_policy_accepted_tsterms_and_privacy_policy_accepted_datetimeproduct_updates_opt_inapi_exp_email_opt_inkey_removal_email_opt_inkeys[]—key_uuid,name(decrypted ornull),role_id,role_name,storage_uuid,storage_name(decrypted ornull)owned_storages[]—storage_uuid,name(decrypted ornull)
curl -X GET https://app.omega-safe.com/users/me \
-H "Accept: application/json" \
-H "Authorization: Bearer YOUR_TOKEN"Billing Documents#
Download billing documents for an authenticated user’s own completed orders.
Endpoints
GET /users/orders/<sale_uuid>/receipt.pdf— return the receipt PDF for a saleGET /users/orders/<sale_uuid>/invoice.pdf— return the invoice PDF snapshot for a sale when one exists
Auth: Required | Role: User | API key: No | Mode: File download
Both routes accept Bearer-token authentication or a valid session cookie. They return 404 Not Found when sale_uuid is invalid or the order does not belong to the authenticated user. The invoice route also returns 404 Not Found when the sale has no invoice snapshot.
curl -X GET "https://app.omega-safe.com/users/orders/123e4567-e89b-12d3-a456-426614174000/receipt.pdf" \
-H "Authorization: Bearer YOUR_TOKEN" \
-o receipt.pdf5. Keys API#
For the full user-to-key role table, sharing model, unlink matrix, billing transfer eligibility checks, and storage ownership model, see Keys Management — User-to-Key Roles.
User-to-Key Roles#
These roles belong to the authenticated user’s relation to a given key. The key object itself does not carry a role.
| Relation Role | ID | Capabilities |
|---|---|---|
| OWNER | 1 | Encrypt & Decrypt; invite as OWNER, MAINTAINER, or USER; edit ECS name, omegasafe_version, and storage_uuid; delete key; transfer billing when requester is current billing user |
| MAINTAINER | 2 | Encrypt & Decrypt; invite as USER only; edit ECS name and omegasafe_version; cannot change storage_uuid; can make renewals; cannot delete keys or deltas |
| USER | 3 | Encrypt & Decrypt; cannot create key-sharing invitations; cannot use PATCH /keys/edit; can access key/delta flows with restricted key metadata visibility; cannot make renewals |
Roles are cumulative: MAINTAINER includes all USER capabilities; OWNER includes all MAINTAINER (and therefore USER) capabilities.
Data Visibility and Masking#
For key relations with role USER (key_role_id=3), these fields are masked to the string value "Forbidden":
storage_uuidstorage_namealpha_global_position_capacityalpha_latest_claimed_global_position
For internal storage keys, storage_uuid is exposed as the constant public view UUID 64753caf-2d47-4558-8933-9691b58f9b09 and storage_name is exposed as the service display name. The underlying internal storage UUID is intentionally hidden.
Storage Ownership Model#
See Keys Management — Storage Ownership Model for the full storage ownership model. In brief:
- At key registration time,
storage_uuidcan point only to the public Internal storage view UUID or to an external storage owned by the authenticated user. - Only a key OWNER can change
storage_uuid, and only to one of that owner’s own external storages. PATCH /keys/editdoes not support moving a key back to Internal storage.- Changing storage does not copy, move, rename, or delete existing Delta objects. Download existing Deltas first, place them into the target storage at
{remote_path}/{key_uuid}/{delta_uuid}(or{key_uuid}/{delta_uuid}whenremote_pathis empty), then switch the key metadata. See Bring Your Own Storage — Object Layout for the full object layout.
POST /keys/register#
Create a key.
Auth: Required | Role: User | API key: Allowed (requires keys:write scope) | Mode: Hybrid (HTML + JSON)
Request body:
{
"key_uuid": "234e5678-e89b-12d3-a456-426614174001",
"name": "prod_key",
"storage_uuid": "123e4567-e89b-12d3-a456-426614174000",
"omegasafe_version": "1.0.0",
"alpha_global_position_capacity": 68417929,
"alpha_latest_claimed_global_position": null
}Parameters:
key_uuid(UUID, required)name(string, required): initial ECS key name; max 255, safe-char format (A-Z a-z 0-9 . _ -)storage_uuid(UUID, required): must be either the Internal-storage public UUID or an external storage UUID owned by the authenticated useromegasafe_version(string, required): max 50, safe-char format (A-Z a-z 0-9 . _ -)alpha_global_position_capacity(integer, required):68417929..18446744073709551615alpha_latest_claimed_global_position(integer or null, optional):nullor0..18446744073709551615; must be< alpha_global_position_capacity
Response (201 Created):
{
"message": "Key with key_uuid=234e5678-e89b-12d3-a456-426614174001 registered successfully."
}Errors:
403 Forbidden— billing expired or no free billing slots404 Not Found—storage_uuiddoes not exist, is not owned by the authenticated user, or refers to an internal storage UUID other than the public Internal-storage view UUID409 Conflict— key UUID already exists422 Unprocessable Entity— validation error
curl -X POST https://app.omega-safe.com/keys/register \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"key_uuid": "234e5678-e89b-12d3-a456-426614174001",
"name": "prod_key",
"storage_uuid": "123e4567-e89b-12d3-a456-426614174000",
"omegasafe_version": "1.0.0",
"alpha_global_position_capacity": 68417929,
"alpha_latest_claimed_global_position": null
}'GET /keys/list#
List all keys related to the authenticated user.
Auth: Required | Role: User | API key: Allowed (requires keys:read scope) | Mode: Hybrid (HTML + JSON)
Response (200 OK):
{
"keys": [
{
"key_uuid": "234e5678-e89b-12d3-a456-426614174001",
"name": "prod_key",
"storage_uuid": "123e4567-e89b-12d3-a456-426614174000",
"storage_name": "my_s3_bucket",
"storage_is_internal": false,
"omegasafe_version": "1.0.0",
"alpha_global_position_capacity": 68417929,
"alpha_latest_claimed_global_position": 1024,
"key_role_id": 1,
"key_role_name": "Owner",
"relation_uuid": "7d68476f-b87a-4f01-9188-e01d321f2505",
"billing_user_uuid": "456e7890-e89b-12d3-a456-426614174003",
"billing_user_email": "owner@example.com",
"billing_exp_ts": 1767225600,
"billing_exp_datetime": "2026-01-01T00:00:00+00:00",
"billing_expired": false,
"is_billing_user": true
}
]
}name in this response is the current ECS key name. The immutable local Alpha key name is not stored in the server API.
For role USER, restricted fields are masked with "Forbidden".
curl -X GET https://app.omega-safe.com/keys/list \
-H "Accept: application/json" \
-H "Authorization: Bearer YOUR_TOKEN"POST /keys/by_uuid#
Fetch one key by UUID.
Auth: Required | Role: User | API key: Allowed (requires keys:read scope) | Mode: API (JSON only)
Request body:
{
"key_uuid": "234e5678-e89b-12d3-a456-426614174001"
}Response (200 OK): Same schema as a single entry from GET /keys/list.
name in this response is the current ECS key name.
Errors:
404 Not Found— key does not exist, or the authenticated user’s relation to this key does not exist422 Unprocessable Entity— validation error
PATCH /keys/edit#
Apply a partial update to a key.
Auth: Required | Role: User (key role must be Owner or Maintainer for metadata edits; storage changes require Owner) | API key: Allowed (requires keys:write scope) | Mode: Hybrid (HTML + JSON)
Request body:
{
"key_uuid": "234e5678-e89b-12d3-a456-426614174001",
"name": "updated_key_name",
"storage_uuid": "123e4567-e89b-12d3-a456-426614174000",
"omegasafe_version": "1.0.1"
}Parameters:
key_uuid(UUID, required)name(string, optional): new ECS key name; max 255, safe-char formatstorage_uuid(UUID, optional): owner-only; if provided it must be an external storage UUID owned by the authenticated useromegasafe_version(string, optional): max 50, safe-char format
At least one of name, storage_uuid, or omegasafe_version must be provided.
Editing name updates only the ECS-side display name. It does not rewrite local Alpha metadata on user devices.
Response (200 OK):
{
"message": "Key has been modified successfully."
}Errors:
403 Forbidden— insufficient key permission, or MAINTAINER attempted to changestorage_uuid404 Not Found— key does not exist, or submittedstorage_uuiddoes not exist / is not owned by the authenticated user / points to Internal storage422 Unprocessable Entity— validation error
POST /keys/delete (DELETE /keys/delete)#
Delete a key and remove all related deltas from the database. Cloud object deletion is attempted per delta.
Endpoint: DELETE /keys/delete
Auth: Required | Role: User (key role must be Owner) | API key: Allowed (requires keys:delete scope) | Mode: Hybrid (HTML + JSON)
Request body:
{
"key_uuid": "234e5678-e89b-12d3-a456-426614174001"
}Response (200 OK):
{
"message": "Key with key_uuid=234e5678-e89b-12d3-a456-426614174001 was successfully deleted from the database."
}Behavior notes:
- Message may append cloud-deletion status details for Delta removal (success/failure list).
- Key deletion from the database still completes even if some cloud deletions fail.
- Notification emails are queued for related users who opted in (
key_removal_email_opt_in=true), excluding the deleting user.
Errors:
403 Forbidden— requester is not key owner404 Not Found— key does not exist, or the authenticated user’s relation to this key does not exist422 Unprocessable Entity— validation error
POST /keys/unlink#
Remove a key relation from a key without deleting the key itself.
Auth: Required | Role: User (relation and role rules apply) | API key: No | Mode: Hybrid (HTML + JSON)
Request body:
{
"relation_uuid": "7d68476f-b87a-4f01-9188-e01d321f2505"
}Response (200 OK):
{
"message": "UserKeyRelation ukr_uuid=7d68476f-b87a-4f01-9188-e01d321f2505 for user_uuid=456e7890-e89b-12d3-a456-426614174003 and key_uuid=234e5678-e89b-12d3-a456-426614174001 was successfully removed."
}See Keys Management — Unlink Matrix for the full unlink matrix.
Errors:
403 Forbidden— unlink conditions not met404 Not Found—relation_uuiddoes not exist, or the authenticated user’s relation to the affected key does not exist409 Conflict— billing-user relation unlink blocked, or sole-owner self-unlink blocked422 Unprocessable Entity— validation error
PATCH /keys/share#
Create a key-sharing invitation.
Auth: Required | Role: User (key role must be Owner or Maintainer) | API key: No | Mode: Hybrid (HTML + JSON)
Request body:
{
"key_uuid": "234e5678-e89b-12d3-a456-426614174001",
"email": "colleague@example.com",
"key_role_id": 3
}Parameters:
key_uuid(UUID, required)email(email string, required)key_role_id(integer, required): one of1,2,3
Role grant rule: Owner can grant any role (1, 2, 3); Maintainer can grant only role 3.
Response (201 Created):
{
"message": "Invitation to share key_uuid=234e5678-e89b-12d3-a456-426614174001 has been created. The recipient can accept it after signing in with that email, even when the notification email is delayed by abuse-prevention limits."
}Errors:
403 Forbidden— insufficient key permission or invalid grant level404 Not Found— key does not exist, or the authenticated user’s relation to this key does not exist409 Conflict— invitation already exists for(email_hash, key_id)422 Unprocessable Entity— validation error
GET /keys/invitations/list#
List incoming and outgoing invitations for the current user.
Auth: Required | Role: User | API key: Allowed (requires invitations:read scope) | Mode: Hybrid (HTML + JSON)
Response (200 OK):
{
"invitations_for_user": [
{
"invitation_uuid": "345e6789-e89b-12d3-a456-426614174002",
"key_uuid": "234e5678-e89b-12d3-a456-426614174001",
"key_name": "prod_key",
"key_role_id": 2,
"key_role_name": "Maintainer",
"sender_uuid": "777e6789-e89b-12d3-a456-426614174111",
"sender_email": "owner@example.com",
"email": "invitee@example.com"
}
],
"invitations_by_user": [
{
"invitation_uuid": "456e7890-e89b-12d3-a456-426614174003",
"key_uuid": "567e8901-e89b-12d3-a456-426614174004",
"key_name": "staging_key",
"key_role_id": 3,
"key_role_name": "User",
"sender_uuid": "777e6789-e89b-12d3-a456-426614174111",
"sender_email": "owner@example.com",
"email": "new.user@example.com"
}
]
}POST /keys/invitation_accept#
Accept a pending invitation.
Auth: Required | Role: User (invitee only) | API key: No | Mode: Hybrid (HTML + JSON)
Request body:
{
"invitation_uuid": "345e6789-e89b-12d3-a456-426614174002"
}Response (200 OK):
{
"message": "Invitation invitation_uuid=345e6789-e89b-12d3-a456-426614174002 for key_uuid=234e5678-e89b-12d3-a456-426614174001 was successfully accepted. key_role_name=maintainer"
}If the invitee already has a relation to the key, the endpoint still returns 200 OK with message text indicating the invitation was already accepted, and removes the stale invitation row.
Errors:
404 Not Found—invitation_uuiddoes not exist, or the authenticated user is not the invitee422 Unprocessable Entity— validation error
DELETE /keys/invitation_delete#
Delete a pending invitation.
Auth: Required | Role: User (allowed if invitee, inviter, or key owner) | API key: No | Mode: Hybrid (HTML + JSON)
Request body:
{
"invitation_uuid": "345e6789-e89b-12d3-a456-426614174002"
}Response (200 OK):
{
"message": "Invitation with invitation_uuid=345e6789-e89b-12d3-a456-426614174002 for key_uuid=234e5678-e89b-12d3-a456-426614174001 was successfully deleted."
}Errors:
403 Forbidden— requester is related to key but not inviter/invitee/owner404 Not Found—invitation_uuiddoes not exist, or requester is unauthorized422 Unprocessable Entity— validation error
PATCH /keys/transfer_billing#
Transfer billing responsibility for one specific key from the current billing user to another owner of that same key.
Auth: Required | Role: User (requester must be current billing user) | API key: No | Mode: Hybrid (HTML + JSON)
Request body:
{
"key_uuid": "234e5678-e89b-12d3-a456-426614174001",
"target_user_uuid": "456e7890-e89b-12d3-a456-426614174003"
}Response (200 OK):
{
"message": "Billing successfully transferred."
}Eligibility checks (all seven required):
- Requester is current billing user
- Target user exists and is not requester
- Target user account is activated
- Target user is owner of this key
- Target user is not in unpaid trial
- Target user has active subscription (
billing_exp_tsnot expired) - Target user has at least one free billing slot
Errors:
403 Forbidden— requester is not billing user, or self-transfer attempted404 Not Found—key_uuiddoes not exist, ortarget_user_uuiddoes not exist409 Conflict— target eligibility check failed422 Unprocessable Entity— validation error
PATCH /keys/claim_alpha#
Claim capacity from a key’s alpha space.
Auth: Required | Role: User (key role must be Owner or Maintainer) | API key: Allowed (requires keys:write scope) | Mode: Hybrid (HTML + JSON)
Alpha is the key’s first iteration and is reserved for encrypting subsequent key iterations called Deltas. See Encryption Method — Alpha and Delta Split. It is tracked on the key record through alpha_global_position_capacity and alpha_latest_claimed_global_position. Alpha positions are 0 .. alpha_global_position_capacity - 1. The first Delta for the key must start at alpha_global_position_capacity.
Request body:
{
"key_uuid": "234e5678-e89b-12d3-a456-426614174001",
"claim_size_bytes": 500000
}Parameters:
key_uuid(UUID, required)claim_size_bytes(integer, required):1..18446744073709551615
Response (200 OK):
{
"message": "successfully claimed 500000 positions from alpha.",
"key_uuid": "234e5678-e89b-12d3-a456-426614174001",
"claim_start_global_position": 0,
"claim_end_global_position": 499999,
"remaining_capacity_bytes": 67917929
}Errors:
403 Forbidden— insufficient role on the key, or key billing subscription expired404 Not Found— key does not exist, or the authenticated user’s relation to this key does not exist409 Conflict—claim_size_bytesexceeds currently available capacity422 Unprocessable Entity— validation error
POST /keys/internal_storage_limits#
Return usage stats for internal-storage keys.
Auth: Required | Role: User | API key: Allowed (requires keys:read scope) | Mode: Hybrid (HTML + JSON)
Request body:
{
"key_uuid": "234e5678-e89b-12d3-a456-426614174001"
}Response (200 OK, internal-storage key):
{
"key_uuid": "234e5678-e89b-12d3-a456-426614174001",
"usage": {
"uploads": {
"daily": { "used": 1, "limit": 2, "remaining": 1, "percentage": 50, "has_limit": true },
"monthly": { "used": 3, "limit": 5, "remaining": 2, "percentage": 60, "has_limit": true },
"stored": { "used": 3, "limit": 15, "remaining": 12, "percentage": 20, "has_limit": true }
},
"downloads": {
"daily": { "used": 1, "limit": 20, "remaining": 19, "percentage": 5, "has_limit": true },
"monthly": { "used": 4, "limit": 200, "remaining": 196, "percentage": 2, "has_limit": true }
}
}
}Usage metric fields per bucket: used (integer count), limit (integer or null when disabled), remaining (integer or null when disabled), percentage (integer 0–100), has_limit (boolean).
Response (200 OK, non-internal key):
{
"key_uuid": "234e5678-e89b-12d3-a456-426614174001",
"message": "Key uses user-provided storage; usage is not tracked."
}These limits apply only when a key uses OmegaSafe-managed internal storage. They are cost-safety controls, not a paid upgrade boundary. See Bring Your Own Storage — Internal Storage Limits for the full internal storage limits and BYOS guidance.
Errors:
404 Not Found— key does not exist, or the authenticated user’s relation to this key does not exist422 Unprocessable Entity— validation error
6. Deltas API#
Deltas are sequential key iterations for keys. See Encryption Method — Alpha and Delta Split. Each Delta has a unique delta_uuid and a sequential seq_number. Delta ranges must be continuous with no gaps. The first Delta must start at alpha_global_position_capacity.
Upload Workflow#
Delta upload is a three-step process:
- Get a presigned upload URL (
POST /deltas/upload_url) - Upload the file directly to S3 or R2 using the presigned URL
- Confirm the upload so OmegaSafe creates the Delta record (
POST /deltas/confirm_upload)
Step 1: POST /deltas/upload_url#
Request a presigned URL for uploading a delta file.
Auth: Required | Role: User (key role: Owner or Maintainer) | API key: Allowed (requires keys:write scope) | Mode: Hybrid (JSON response for both API clients and browser upload flows)
Request body:
{
"key_uuid": "234e5678-e89b-12d3-a456-426614174001",
"delta_uuid": "567e8901-e89b-12d3-a456-426614174004",
"seq_number": 3,
"start_global_position": 3000001,
"end_global_position": 4000000,
"omegasafe_version": "1.0.0",
"latest_claimed_global_position": 3000000,
"file_size_bytes": 5242880,
"content_md5": "XrY7u+Ae7tCTyyK7j1rNww=="
}Parameters:
key_uuid(UUID, required)delta_uuid(UUID, required) — must not already be registeredseq_number(integer, required) — must be between1and16777215; must be1for the first delta, otherwise must equal the latest deltaseq_number + 1start_global_position(integer, required) — must be between1and18446744073709551615, must be smaller thanend_global_position, and must continue the previous range exactly; for the first delta, must equalalpha_global_position_capacityend_global_position(integer, required) — must be between2and18446744073709551615and must be greater thanstart_global_positionomegasafe_version(string, required) — 1 to 50 characters; allowed characters: letters, digits,.,_,-latest_claimed_global_position(integer, optional, nullable) — if provided, must be between1and18446744073709551615and must be within the submitted delta rangefile_size_bytes(integer, required) — exact Delta file size from1to10485760bytes (10 MiB)content_md5(string, required) — MD5 digest of the complete Delta file, encoded as canonical Base64
Response (200 OK):
{
"message": "Presigned upload URL generated successfully",
"upload_url": "https://bucket.s3.amazonaws.com/path/to/file?X-Amz-...",
"upload_method": "PUT",
"upload_headers": {
"Content-Type": "application/octet-stream",
"Content-Length": "5242880",
"Content-MD5": "XrY7u+Ae7tCTyyK7j1rNww==",
"If-None-Match": "*",
"x-amz-meta-expected-size-bytes": "5242880",
"x-amz-meta-expected-content-md5": "XrY7u+Ae7tCTyyK7j1rNww==",
"x-amz-meta-upload-attempt": "89abcdef-0123-4567-89ab-cdef01234567"
},
"expires_in": 900,
"max_file_size": 10485760,
"key_uuid": "234e5678-e89b-12d3-a456-426614174001",
"delta_uuid": "567e8901-e89b-12d3-a456-426614174004",
"upload_attempt_uuid": "89abcdef-0123-4567-89ab-cdef01234567",
"metadata": {
"seq_number": 3,
"start_global_position": 3000001,
"end_global_position": 4000000,
"latest_claimed_global_position": 3000000
}
}expires_in and max_file_size are returned by the OmegaSafe API. The configured values are 900 seconds and 10485760 bytes (10 MiB). The exact submitted size, Content-MD5, If-None-Match: *, expected-size metadata, expected-checksum metadata, and upload-attempt metadata are part of the signed PUT request. S3 or R2 verifies the body against Content-MD5 before accepting it. The OmegaSafe API does not receive the file body.
upload_attempt_uuid identifies this presigned upload attempt. Its value matches the signed x-amz-meta-upload-attempt header and must be sent when confirming the upload.
Errors:
403 Forbidden— not an Owner or Maintainer, or the key billing subscription is expired404 Not Found— key not found, no access to the key, or the associated storage record is missing409 Conflict— delta UUID already exists, the object already exists in storage,seq_numberis invalid, or the submitted range is not contiguous429 Too Many Requests— internal storage upload safety limits were reached502 Bad Gateway— the server failed to generate the presigned upload URL422 Unprocessable Entity— request payload failed schema validation
curl -X POST https://app.omega-safe.com/deltas/upload_url \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "X-Api-Key: YOUR_API_KEY" \
-d '{
"key_uuid": "234e5678-e89b-12d3-a456-426614174001",
"delta_uuid": "567e8901-e89b-12d3-a456-426614174004",
"seq_number": 3,
"start_global_position": 3000001,
"end_global_position": 4000000,
"omegasafe_version": "1.0.0",
"latest_claimed_global_position": 3000000,
"file_size_bytes": 5242880,
"content_md5": "XrY7u+Ae7tCTyyK7j1rNww=="
}'Step 2: Upload File#
Upload the delta file directly to S3 or R2 using the presigned URL from Step 1.
- Use the
upload_urlfrom Step 1. - Use the
upload_methodfrom Step 1 (currentlyPUT). - Include the headers returned in
upload_headers. - Send raw binary file data.
UPLOAD_URL="https://bucket.s3.amazonaws.com/path/to/file?X-Amz-..."
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: application/octet-stream" \
-H "Content-Length: 5242880" \
-H "Content-MD5: XrY7u+Ae7tCTyyK7j1rNww==" \
-H "If-None-Match: *" \
-H "x-amz-meta-expected-size-bytes: 5242880" \
-H "x-amz-meta-expected-content-md5: XrY7u+Ae7tCTyyK7j1rNww==" \
-H "x-amz-meta-upload-attempt: 89abcdef-0123-4567-89ab-cdef01234567" \
--data-binary @delta-file.binCalculate content_md5 from the complete local file before Step 1. The URL is valid for the number of seconds returned in expires_in. Send every returned header unchanged and upload exactly the signed Content-Length. The maximum accepted upload size is the max_file_size value returned by Step 1.
After one successful PUT, reuse of the URL cannot overwrite the object and returns 412 Precondition Failed. When a PUT returns 412, continue to Step 3 with the upload_attempt_uuid returned by Step 1. Confirmation accepts the existing object only when its signed upload-attempt metadata and integrity attributes match that attempt.
Step 3: POST /deltas/confirm_upload#
Confirm that the upload completed and create the Delta record.
Auth: Required | Role: User (key role: Owner or Maintainer) | API key: Allowed (requires keys:write scope) | Mode: Hybrid (HTML redirect or JSON)
Request body:
{
"key_uuid": "234e5678-e89b-12d3-a456-426614174001",
"delta_uuid": "567e8901-e89b-12d3-a456-426614174004",
"seq_number": 3,
"start_global_position": 3000001,
"end_global_position": 4000000,
"omegasafe_version": "1.0.0",
"latest_claimed_global_position": 3000000,
"upload_attempt_uuid": "89abcdef-0123-4567-89ab-cdef01234567"
}The metadata parameters are the same as Step 1, except file_size_bytes and content_md5 are not accepted. upload_attempt_uuid (UUID, required) must be the value returned by Step 1.
Behavior notes:
- The OmegaSafe API verifies the completed object’s signed upload-attempt, size, and checksum metadata before creating the Delta record. It also compares a reliable single-part ETag with the expected MD5 when available.
- Missing or mismatched upload-attempt metadata returns
409 Conflictand preserves the existing object. This prevents one upload attempt from claiming or deleting an object created by another attempt. - If the upload attempt matches but the size or checksum attributes are invalid, confirmation fails and OmegaSafe attempts to delete the invalid object.
- If
latest_claimed_global_positionis omitted, the stored value becomesstart_global_position - 1.
Response (201 Created):
{
"message": "delta_uuid=567e8901-e89b-12d3-a456-426614174004 for key_uuid=234e5678-e89b-12d3-a456-426614174001 uploaded and created successfully.",
"delta_uuid": "567e8901-e89b-12d3-a456-426614174004",
"key_uuid": "234e5678-e89b-12d3-a456-426614174001"
}Errors:
424 Failed Dependency— the object was not found in BYOS storage, or completed-object integrity validation failed403 Forbidden— not an Owner or Maintainer404 Not Found— key not found, no access to the key, or the associated storage record is missing409 Conflict— delta already exists, the submitted sequence/range is no longer contiguous at commit time, or the stored upload-attempt metadata does not match this confirmation attempt502 Bad Gateway— storage verification failed, or the object is missing from internal storage422 Unprocessable Entity— request payload failed schema validation
Complete Upload Shell Script Example#
#!/bin/bash
set -euo pipefail
KEY_UUID="234e5678-e89b-12d3-a456-426614174001"
DELTA_UUID="$(uuidgen)"
SEQ_NUMBER=3
START_POS=3000001
END_POS=4000000
LATEST_CLAIMED=3000000
FILE_PATH="delta-file.bin"
API_KEY="YOUR_API_KEY"
FILE_SIZE_BYTES="$(wc -c < "$FILE_PATH" | tr -d ' ')"
CONTENT_MD5="$(openssl dgst -md5 -binary "$FILE_PATH" | base64 | tr -d '\n')"
UPLOAD_RESPONSE="$(curl -sS -X POST https://app.omega-safe.com/deltas/upload_url \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "X-Api-Key: $API_KEY" \
-d "{
\"key_uuid\": \"$KEY_UUID\",
\"delta_uuid\": \"$DELTA_UUID\",
\"seq_number\": $SEQ_NUMBER,
\"start_global_position\": $START_POS,
\"end_global_position\": $END_POS,
\"omegasafe_version\": \"1.0.0\",
\"latest_claimed_global_position\": $LATEST_CLAIMED,
\"file_size_bytes\": $FILE_SIZE_BYTES,
\"content_md5\": \"$CONTENT_MD5\"
}")"
UPLOAD_URL="$(echo "$UPLOAD_RESPONSE" | jq -r '.upload_url')"
CONTENT_LENGTH="$(echo "$UPLOAD_RESPONSE" | jq -r '.upload_headers["Content-Length"]')"
SIGNED_CONTENT_MD5="$(echo "$UPLOAD_RESPONSE" | jq -r '.upload_headers["Content-MD5"]')"
IF_NONE_MATCH="$(echo "$UPLOAD_RESPONSE" | jq -r '.upload_headers["If-None-Match"]')"
EXPECTED_SIZE="$(echo "$UPLOAD_RESPONSE" | jq -r '.upload_headers["x-amz-meta-expected-size-bytes"]')"
EXPECTED_CONTENT_MD5="$(echo "$UPLOAD_RESPONSE" | jq -r '.upload_headers["x-amz-meta-expected-content-md5"]')"
UPLOAD_ATTEMPT_HEADER="$(echo "$UPLOAD_RESPONSE" | jq -r '.upload_headers["x-amz-meta-upload-attempt"]')"
UPLOAD_ATTEMPT_UUID="$(echo "$UPLOAD_RESPONSE" | jq -r '.upload_attempt_uuid')"
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: application/octet-stream" \
-H "Content-Length: $CONTENT_LENGTH" \
-H "Content-MD5: $SIGNED_CONTENT_MD5" \
-H "If-None-Match: $IF_NONE_MATCH" \
-H "x-amz-meta-expected-size-bytes: $EXPECTED_SIZE" \
-H "x-amz-meta-expected-content-md5: $EXPECTED_CONTENT_MD5" \
-H "x-amz-meta-upload-attempt: $UPLOAD_ATTEMPT_HEADER" \
--data-binary @"$FILE_PATH"
curl -X POST https://app.omega-safe.com/deltas/confirm_upload \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "X-Api-Key: $API_KEY" \
-d "{
\"key_uuid\": \"$KEY_UUID\",
\"delta_uuid\": \"$DELTA_UUID\",
\"seq_number\": $SEQ_NUMBER,
\"start_global_position\": $START_POS,
\"end_global_position\": $END_POS,
\"omegasafe_version\": \"1.0.0\",
\"latest_claimed_global_position\": $LATEST_CLAIMED,
\"upload_attempt_uuid\": \"$UPLOAD_ATTEMPT_UUID\"
}"
echo "Delta uploaded successfully: $DELTA_UUID"POST /deltas/download_url#
Request a presigned URL for downloading a delta file.
Auth: Required | Role: User (key access required) | API key: Allowed (requires keys:read scope) | Mode: Hybrid (HTML redirect or JSON)
There is no billing check on this route. Any user with access to a key can always download Deltas regardless of subscription status. See Subscription Model — Decryption Is Always Available.
Request body:
{
"key_uuid": "234e5678-e89b-12d3-a456-426614174001",
"delta_uuid": "345e6789-e89b-12d3-a456-426614174002"
}Response (200 OK, JSON):
{
"message": "Presigned download URL generated successfully",
"download_url": "https://bucket.s3.amazonaws.com/path/to/file?X-Amz-...",
"expires_in": 900,
"key_uuid": "234e5678-e89b-12d3-a456-426614174001",
"delta_uuid": "345e6789-e89b-12d3-a456-426614174002"
}Errors:
404 Not Found— key not found, no access to the key, or delta not found429 Too Many Requests— internal storage download safety limits were reached502 Bad Gateway— the server failed to generate the presigned download URL422 Unprocessable Entity— request payload failed schema validation
PATCH /deltas/claim#
Claim capacity from a delta.
Auth: Required | Role: User (key access required) | API key: Allowed (requires keys:write scope) | Mode: Hybrid (PATCH supports HTML form submission and JSON)
Request body:
{
"key_uuid": "234e5678-e89b-12d3-a456-426614174001",
"delta_uuid": "345e6789-e89b-12d3-a456-426614174002",
"claim_size_bytes": 500000
}Parameters:
key_uuid(UUID, required)delta_uuid(UUID, required)claim_size_bytes(integer, required) — must be between1and18446744073709551615; if it exceeds the currently available capacity in the delta, the request fails with409 Conflict
Claim routes are serialized with row-level locking on the target delta to avoid overlapping claims.
Response (200 OK, JSON):
{
"message": "successfully claimed 500000 positions from delta",
"key_uuid": "234e5678-e89b-12d3-a456-426614174001",
"delta_uuid": "345e6789-e89b-12d3-a456-426614174002",
"claim_start_global_position": 1500001,
"claim_end_global_position": 2000000,
"remaining_capacity_bytes": 0
}Errors:
403 Forbidden— the key billing subscription is expired404 Not Found— key not found, no access to the key, or delta not found409 Conflict—claim_size_bytesexceeds the currently available capacity422 Unprocessable Entity— request payload failed schema validation
DELETE /deltas/delete#
Delete a delta database record. The server then attempts a best-effort object deletion from storage.
Auth: Required | Role: User (key role: Owner only) | API key: Allowed (requires keys:delete scope) | Mode: Hybrid (HTML redirect or JSON)
Request body:
{
"key_uuid": "234e5678-e89b-12d3-a456-426614174001",
"delta_uuid": "345e6789-e89b-12d3-a456-426614174002"
}Response (200 OK):
{
"message": "delta_uuid=345e6789-e89b-12d3-a456-426614174002 reference was successfully deleted from the database.",
"delta_uuid": "345e6789-e89b-12d3-a456-426614174002",
"key_uuid": "234e5678-e89b-12d3-a456-426614174001"
}Behavior notes:
- Only the key owner can delete a delta.
- Only the most recent delta for a key can be deleted.
- If storage object deletion fails, the API still returns
200 OKand appends a warning message tomessage.
Errors:
403 Forbidden— only the key owner can delete deltas404 Not Found— key not found, no access to the key, or delta not found409 Conflict— the target delta is not the most recent delta for the key422 Unprocessable Entity— request payload failed schema validation
POST /deltas/list#
List all deltas for a key.
Auth: Required | Role: User (key access required) | API key: Allowed (requires keys:read scope) | Mode: Hybrid (HTML page or JSON)
Request body:
{
"key_uuid": "234e5678-e89b-12d3-a456-426614174001"
}Response (200 OK, JSON):
[
{
"delta_uuid": "345e6789-e89b-12d3-a456-426614174002",
"key_uuid": "234e5678-e89b-12d3-a456-426614174001",
"key_name": "Production Key",
"seq_number": 1,
"start_global_position": 1000000,
"end_global_position": 2000000,
"latest_claimed_global_position": 1500000
}
]Errors:
404 Not Found— key not found or you do not have access to it422 Unprocessable Entity— request payload failed schema validation
7. Storages API#
The Storages API configures where Deltas are uploaded, downloaded, and deleted from, as part of safe renewal and key-sharing flows. For the full object layout, IAM/CORS examples, and BYOS guidance, see Bring Your Own Storage — Provider Configuration.
Storage Types#
| Provider | provider value | storage_type_id | storage_type_name |
|---|---|---|---|
| Cloudflare R2 | r2 | 1 | r2 |
| AWS S3 | s3 | 2 | s3 |
Internal storage is masked in serialized responses: storage_uuid becomes the fixed placeholder UUID 64753caf-2d47-4558-8933-9691b58f9b09, storage_type_id becomes 0, and storage_type_name becomes unknown.
Serialized Storage Object#
{
"storage_uuid": "123e4567-e89b-12d3-a456-426614174000",
"name": "production_s3",
"provider": "s3",
"bucket_name": "my-production-bucket",
"region": "us-east-1",
"endpoint_url": null,
"storage_type_id": 2,
"storage_type_name": "s3",
"remote_path": "omega-safe/deltas",
"is_internal": false
}Secrets are never returned. S3 storages return endpoint_url: null. R2 storages always return region: "auto".
Object Layout#
When remote_path is empty or null, the effective object key is {key_uuid}/{delta_uuid}. When remote_path is set, the effective object key is {remote_path}/{key_uuid}/{delta_uuid}. Empty path segments inside remote_path are removed when object keys are built. See Bring Your Own Storage — Editing Storage Settings for the full layout and migration guidance.
Provider-Specific Rules#
provider: "r2"requiresendpoint_urland requiresregion: "auto".provider: "s3"rejects any non-emptyendpoint_urland rejectsregion: "auto".
GET /storages/bucket_add and POST /storages/bucket_add#
Add a new external R2 or S3 storage bucket.
Auth: Required | Role: User | API key: Allowed (requires storage:write scope) | Mode: Hybrid (HTML form + JSON)
GET /storages/bucket_add returns metadata describing the supported providers in JSON mode.
POST request body (R2 example):
{
"name": "production_r2",
"provider": "r2",
"bucket_name": "my-r2-bucket",
"region": "auto",
"access_key_id": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"secret_access_key": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6",
"endpoint_url": "https://ACCOUNT-ID.r2.cloudflarestorage.com",
"remote_path": "omega-safe/deltas"
}POST request body (S3 example):
{
"name": "production_s3",
"provider": "s3",
"bucket_name": "my-production-bucket",
"region": "us-east-1",
"access_key_id": "AKIAIOSFODNN7EXAMPLE",
"secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"remote_path": "omega-safe/deltas"
}Parameters:
name(string, required): max 255 characters; only letters, digits,.,_, and-allowedprovider(string, required):r2ors3bucket_name(string, required): 3–63 characters, lowercase letters/numbers/hyphens/periods only, must start and end with a lowercase letter or number, must not contain.., and must not be an IPv4 address stringregion(string, required): one of the supported AWS region codes, orautofor R2access_key_id(string, required): 16–128 characters,[A-Za-z0-9+/=]onlysecret_access_key(string, required): 32–128 characters,[A-Za-z0-9+/=]onlyendpoint_url(string, optional): must start withhttps://when provided; required for R2; must be omitted or empty for S3remote_path(string, optional): max 1024 characters; must be relative, must not contain.., must not start with/, and must not contain<,>,:,",|,?, or*
Response (201 Created):
{
"message": "Storage bucket storage_uuid=123e4567-e89b-12d3-a456-426614174000 has been added successfully."
}On success, the JSON response also includes optional warning when LIST, PUT, and GET succeed but DELETE appears unavailable:
{
"message": "Storage bucket storage_uuid=123e4567-e89b-12d3-a456-426614174000 has been added successfully.",
"warning": "DELETE permission appears to be missing for the provided bucket credentials. This is acceptable for OmegaSafe BYOS storage, but explicit Delta or key deletions may leave bucket objects behind for manual cleanup. Details: DELETE permission test failed: AccessDenied - ..."
}Connectivity test details:
The route always tests the bucket before saving. The test uses the fixed object key omegasafe_connection_test.json under the configured remote_path when present, otherwise at bucket root. The route first tests LIST access, then uploads the test file with PUT, downloads it with GET, and compares the SHA-256 hash of the downloaded bytes with the hash of the uploaded bytes. DELETE is attempted last. If DELETE fails, the route still succeeds because DELETE is optional for BYOS, but it returns or flashes a warning. The effective verification order is LIST, PUT, GET, then optional DELETE.
IAM/permission guidance:
For S3, the IAM identity should allow s3:ListBucket on the bucket, s3:GetObject and s3:PutObject on bucket objects, and optionally s3:DeleteObject for explicit Delta or key deletions. See Bring Your Own Storage — S3 IAM Policy for the full minimal IAM policy JSON and CORS examples.
For R2, the credentials should allow the S3-compatible equivalent of bucket LIST, object GET, object PUT, and optionally object DELETE. See Bring Your Own Storage — R2 Permissions for the full R2 permission guidance and CORS examples.
Errors:
422 Unprocessable Entity— validation failed400 Bad Request— bucket connectivity test failed
curl -X POST https://app.omega-safe.com/storages/bucket_add \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "X-Api-Key: YOUR_API_KEY" \
-d '{
"name": "production_r2",
"provider": "r2",
"bucket_name": "my-r2-bucket",
"region": "auto",
"access_key_id": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"secret_access_key": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6",
"endpoint_url": "https://ACCOUNT-ID.r2.cloudflarestorage.com",
"remote_path": "omega-safe/deltas"
}'GET /storages/list#
List storages owned by the authenticated user.
Auth: Required | Role: User | API key: Allowed (requires storage:read scope) | Mode: Hybrid (HTML + JSON)
Response (200 OK, JSON):
{
"user_uuid": "456e7890-e89b-12d3-a456-426614174003",
"owned_storages": [
{
"storage_uuid": "234e5678-e89b-12d3-a456-426614174001",
"name": "production_r2",
"provider": "r2",
"bucket_name": "my-r2-bucket",
"region": "auto",
"endpoint_url": "https://ACCOUNT-ID.r2.cloudflarestorage.com",
"storage_type_id": 1,
"storage_type_name": "r2",
"remote_path": "omega-safe/deltas",
"is_internal": false
}
]
}POST /storages/by_uuid#
Look up a storage by UUID.
Auth: Required | Role: User | API key: Allowed (requires storage:read scope) | Mode: Hybrid (HTML + JSON)
Request body:
{
"storage_uuid": "123e4567-e89b-12d3-a456-426614174000"
}Response (200 OK, JSON): Serialized storage object (see schema above).
Errors:
404 Not Found— storage is missing or not owned by the authenticated user409 Conflict—storage_uuidis the internal placeholder UUID422 Unprocessable Entity— validation failed
POST /storages/bucket_edit and PATCH /storages/bucket_edit#
View or update an existing owned R2 or S3 storage configuration.
Auth: Required | Role: User | API key: Allowed (requires storage:write scope) | Mode: Hybrid (HTML form + JSON)
POST /storages/bucket_edit returns the edit context for the requested storage in JSON mode.
POST request body:
{
"storage_uuid": "123e4567-e89b-12d3-a456-426614174000"
}PATCH request body (name-only update):
{
"storage_uuid": "123e4567-e89b-12d3-a456-426614174000",
"name": "production_s3_archive"
}PATCH request body (change connection details while keeping current credentials):
{
"storage_uuid": "123e4567-e89b-12d3-a456-426614174000",
"remote_path": "omega-safe/archive",
"access_key_id": "no_change",
"secret_access_key": "no_change"
}Credential rules: to update credentials, provide both access_key_id and secret_access_key. To keep credentials unchanged, omit both fields or send both as no_change. Changing provider from R2 to S3 or vice versa requires new credentials.
Name-only updates skip the connectivity test. When connection details are updated, the route re-tests the bucket with the same LIST, PUT, GET, and optional DELETE sequence used during create.
Response (200 OK):
{
"message": "Bucket storage_uuid=123e4567-e89b-12d3-a456-426614174000 was successfully modified."
}Errors:
404 Not Found— storage is missing or not owned by the authenticated user422 Unprocessable Entity— validation failed, including provider change without new credentials, R2 withoutendpoint_url, R2 with region other thanauto, S3 with a non-emptyendpoint_url, or S3 with regionauto400 Bad Request— connectivity test failed with the effective bucket configuration
DELETE /storages/delete#
Delete a storage configuration.
Auth: Required | Role: User | API key: Allowed (requires storage:delete scope) | Mode: Hybrid (HTML + JSON)
Request body:
{
"storage_uuid": "123e4567-e89b-12d3-a456-426614174000"
}Response (200 OK):
{
"message": "Storage with storage_uuid=123e4567-e89b-12d3-a456-426614174000 was successfully deleted."
}Behavior notes:
- The route deletes the storage row only. It does not enumerate or delete objects from the bucket.
- The route is blocked while any keys still reference that storage.
Errors:
404 Not Found— storage is missing or not owned by the authenticated user422 Unprocessable Entity— validation failed409 Conflict— the storage still has dependent keys
8. Download API#
The Download API covers software release metadata and download flows for OmegaSafe CLI and OmegaSafe Server.
POST /download/cli/latest_version#
Return the latest CLI release for a requested OS/arch pair.
Auth: Required | Role: User | API key: Allowed (requires software:read scope) | Mode: API (JSON only)
Request body:
{
"os": "linux",
"arch": "amd64"
}Response (200 OK):
{
"download_release_uuid": "123e4567-e89b-12d3-a456-426614174000",
"software_type": "cli",
"version": "1.2.3",
"os": "linux",
"arch": "amd64",
"filename": "omegasafe-cli-1.2.3-linux-amd64",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"changelog": "Bug fixes and performance improvements",
"release_time_ts": 1704067200,
"release_time_datetime": "2024-01-01T00:00:00+00:00"
}Errors:
401 Unauthorized— missing/invalid auth credentials403 Forbidden— API key lackssoftware:readscope404 Not Found— no release found for requestedos/arch406 Not Acceptable— HTML-preferred request on this API-only route422 Unprocessable Entity— request validation failed
POST /download/server/latest_version#
Return the latest OmegaSafe Server release for a requested OS/arch pair.
Auth: Required | Role: User | API key: Allowed (requires software:read scope) | Mode: API (JSON only)
Request body:
{
"os": "linux",
"arch": "amd64"
}Response (200 OK):
{
"download_release_uuid": "234e5678-e89b-12d3-a456-426614174001",
"software_type": "server",
"version": "2.0.1",
"os": "linux",
"arch": "amd64",
"filename": "omegasafe-server-2.0.1-linux-amd64",
"sha256": "a1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f67890",
"changelog": "OmegaSafe Server improvements and bug fixes",
"release_time_ts": 1704067200,
"release_time_datetime": "2024-01-01T00:00:00+00:00"
}Errors: Same as POST /download/cli/latest_version.
POST /download/cli/download#
Generate a short-lived presigned URL for downloading a CLI release.
Auth: Required | Role: User | API key: Allowed (requires software:read scope) | Mode: Hybrid (HTML redirect + JSON)
Request body (latest version):
{
"os": "linux",
"arch": "amd64"
}Request body (specific version):
{
"os": "linux",
"arch": "amd64",
"version": "1.2.3"
}Response (200 OK, JSON):
{
"download_url": "https://r2-bucket.cloudflarestorage.com/releases/...?X-Amz-...",
"filename": "omegasafe-cli-1.2.3-linux-amd64",
"version": "1.2.3",
"expires_in": 10
}Errors:
401 Unauthorized— missing/invalid auth credentials403 Forbidden— API key lackssoftware:readscope, or the authenticated account is not activated404 Not Found— matching release not found422 Unprocessable Entity— request validation failed429 Too Many Requests— download safety limits exceeded502 Bad Gateway— the server failed to generate the presigned download URL
POST /download/server/download#
Generate a short-lived presigned URL for downloading an OmegaSafe Server release.
Auth: Required | Role: User | API key: Allowed (requires software:read scope) | Mode: Hybrid (HTML redirect + JSON)
Request body (latest version):
{
"os": "linux",
"arch": "amd64"
}Response (200 OK, JSON):
{
"download_url": "https://r2-bucket.cloudflarestorage.com/releases/...?X-Amz-...",
"filename": "omegasafe-server-2.0.1-linux-amd64",
"version": "2.0.1",
"expires_in": 10
}Errors: Same as POST /download/cli/download.
Complete API Download Shell Script Examples#
CLI download example:
#!/bin/bash
set -euo pipefail
OS="linux"
ARCH="amd64"
API_KEY="YOUR_API_KEY"
VERSION_INFO=$(curl -sS -X POST https://app.omega-safe.com/download/cli/latest_version \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "X-Api-Key: $API_KEY" \
-d "{\"os\": \"$OS\", \"arch\": \"$ARCH\"}")
VERSION=$(echo "$VERSION_INFO" | jq -r '.version')
FILENAME=$(echo "$VERSION_INFO" | jq -r '.filename')
SHA256=$(echo "$VERSION_INFO" | jq -r '.sha256')
DOWNLOAD_RESPONSE=$(curl -sS -X POST https://app.omega-safe.com/download/cli/download \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "X-Api-Key: $API_KEY" \
-d "{\"os\": \"$OS\", \"arch\": \"$ARCH\", \"version\": \"$VERSION\"}")
DOWNLOAD_URL=$(echo "$DOWNLOAD_RESPONSE" | jq -r '.download_url')
curl -sS -L -o "$FILENAME" "$DOWNLOAD_URL"
echo "$SHA256 $FILENAME" | sha256sum -cOmegaSafe Server download example:
#!/bin/bash
set -euo pipefail
OS="linux"
ARCH="amd64"
API_KEY="YOUR_API_KEY"
VERSION_INFO=$(curl -sS -X POST https://app.omega-safe.com/download/server/latest_version \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "X-Api-Key: $API_KEY" \
-d "{\"os\": \"$OS\", \"arch\": \"$ARCH\"}")
VERSION=$(echo "$VERSION_INFO" | jq -r '.version')
FILENAME=$(echo "$VERSION_INFO" | jq -r '.filename')
SHA256=$(echo "$VERSION_INFO" | jq -r '.sha256')
DOWNLOAD_RESPONSE=$(curl -sS -X POST https://app.omega-safe.com/download/server/download \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "X-Api-Key: $API_KEY" \
-d "{\"os\": \"$OS\", \"arch\": \"$ARCH\", \"version\": \"$VERSION\"}")
DOWNLOAD_URL=$(echo "$DOWNLOAD_RESPONSE" | jq -r '.download_url')
curl -sS -L -o "$FILENAME" "$DOWNLOAD_URL"
echo "$SHA256 $FILENAME" | sha256sum -c9. Error Handling#
Error Response Envelope#
JSON clients receive the standard error envelope for all application-defined errors:
{
"error_type": "Not Found",
"error_code": 404,
"error_description": "Storage with storage_uuid=123e4567-e89b-12d3-a456-426614174000 was not found."
}Fields:
error_type(string) — HTTP reason phraseerror_code(integer) — HTTP status codeerror_description(string) — exception description
5xx Redaction Rule#
4xx→ original exception description is returned502→ original exception description is returned- All other
5xx(500,503, etc.) → description is replaced with a generic message:"Apologies for the inconvenience. The application encountered an issue. Please try again later. If the issue persists reach out to <support-email>"
Status Codes#
| Code | Name | Notes |
|---|---|---|
| 400 | Bad Request | Route validation/logic failures, ALTCHA failures |
| 401 | Unauthorized | Auth credential failures |
| 402 | Payment Required | POST /users/capture_card_payment only |
| 403 | Forbidden | Permission, scope, session, or account-state constraints |
| 404 | Not Found | Missing route resource or record |
| 406 | Not Acceptable | Mode mismatch (wrong Accept header for the route) |
| 409 | Conflict | Duplicate/state conflicts |
| 410 | Gone | Expired/missing email verification flow |
| 422 | Unprocessable Entity | parse_schemas validation failures |
| 424 | Failed Dependency | Multi-step dependency failure (e.g. confirm_upload when object not uploaded) |
| 429 | Too Many Requests | Cost-safety/rate limits |
| 502 | Bad Gateway | Upstream/service integration errors |
| 503 | Service Unavailable | Temporary processing failure |
Status Code Examples#
400 Bad Request:
{
"error_type": "Bad Request",
"error_code": 400,
"error_description": "Invalid fields payload. Expected an array of field names."
}401 Unauthorized:
{
"error_type": "Unauthorized",
"error_code": 401,
"error_description": "[AUTH_REQUIRED]: Missing Authorization header. Alternatively auth_token cookie needed."
}Other real messages include [AUTH_REQUIRED]: Token is invalid or expired. and [AUTH_REQUIRED]: API key is invalid or expired.
403 Forbidden:
{
"error_type": "Forbidden",
"error_code": 403,
"error_description": "[AUTH_REQUIRED]: API key lacks required scope: keys:delete"
}Other real messages include This operation requires session authentication. and Only the present billing user can transfer billing.
404 Not Found:
{
"error_type": "Not Found",
"error_code": 404,
"error_description": "Storage with storage_uuid=123e4567-e89b-12d3-a456-426614174000 was not found."
}406 Not Acceptable:
{
"error_type": "Not Acceptable",
"error_code": 406,
"error_description": "the GET method on this route requires Accept=\"application/json\""
}409 Conflict:
{
"error_type": "Conflict",
"error_code": 409,
"error_description": "Release already exists for the specified software_type, version, os, and arch."
}410 Gone:
{
"error_type": "Gone",
"error_code": 410,
"error_description": "The verification code has expired. Please request a new one by resending the verification email."
}422 Unprocessable Entity:
{
"error_type": "Unprocessable Entity",
"error_code": 422,
"error_description": "{'key_uuid': ['Invalid key UUID format.']}"
}Invalid/missing/misplaced request data for schema-decorated routes returns 422, not 400. UUID format failures are schema-level 422 errors.
424 Failed Dependency:
{
"error_type": "Failed Dependency",
"error_code": 424,
"error_description": "Uploaded delta file was not found in storage. key_uuid=... delta_uuid=..."
}429 Too Many Requests:
{
"error_type": "Too Many Requests",
"error_code": 429,
"error_description": "Internal storage download limit reached for this key: <limit> downloads per day. Current usage: <current>. The allowance is shared across every user who can access the key. Key owners should move the key to storage they manage to remove these limits."
}502 Bad Gateway:
{
"error_type": "Bad Gateway",
"error_code": 502,
"error_description": "Failed to verify the delta file in the client storage. error=denied"
}502 is intentionally pass-through — the description is not replaced by the generic message.
500 Internal Server Error and 503 Service Unavailable:
{
"error_type": "Internal Server Error",
"error_code": 500,
"error_description": "Apologies for the inconvenience. The application encountered an issue. Please try again later. If the issue persists reach out to <support-email>"
}Retry Guidance#
Retryable in general (with backoff):
429 Too Many Requests500 Internal Server Error502 Bad Gateway(if upstream issue is transient)503 Service Unavailable
Usually non-retryable without request changes:
400,401,403,404,405,406,409,410,422,424
10. Subscription Status#
GET /users/subscription_status#
Return the current billing and subscription summary for the authenticated user.
Auth: Required | Role: User | API key: No | Mode: API (JSON only)
Response (200 OK):
{
"billing_active": true,
"user_is_on_trial": false,
"billing_exp_ts": 1775001600,
"billing_exp_datetime": "2026-04-01T00:00:00+00:00",
"used_slots": 3,
"total_slots": 5,
"slot_usage_percent": 60,
"subscription_id": "I-ACTIVE123",
"pending_subscription_id": "I-PENDING456",
"pending_slots": 10,
"pending_period_days": 30,
"payment_method": 1,
"has_active_card": false,
"subscription_issue": null,
"subscription_issue_is_terminal": false
}Response fields:
billing_active— whether the subscription is currently activeuser_is_on_trial— whether the account is in the free trial periodbilling_exp_ts— Unix timestamp when billing expiresbilling_exp_datetime— ISO 8601 UTC datetime companion forbilling_exp_tsused_slots— number of key slots currently in usetotal_slots— total purchased key slotsslot_usage_percent— integer percentage of slots usedsubscription_id— active PayPal subscription ID, ornullpending_subscription_id— pending PayPal subscription draft ID, ornullpending_slots— slot count for the pending subscription, ornullpending_period_days— billing period in days for the pending subscription, ornullpayment_method—1= PayPal subscription auto-renewal;2= card auto-renewal managed by the application;null= no recorded payment methodhas_active_card— whether a vaulted card is stored for auto-renewalsubscription_issue— string describing a subscription issue, ornullsubscription_issue_is_terminal— whether the subscription issue is terminal (non-recoverable without user action)
The response is returned with Cache-Control: no-store.
For the full billing model, web purchase flows, cancellation, and VAT rules, see Subscription Model — Purchase Flows.
11. Browser & Interactive Flows (Advanced / Non-API Automation)#
Some routes are hybrid. For programmatic clients, always send Accept: application/json so the route returns the JSON contract instead of the HTML-preferred browser flow.
ALTCHA Interoperability#
GET /altcha/challenge returns a fresh challenge object as JSON. Public routes that require an altcha payload are POST /users/register, POST /users/reset_password_request, POST /users/reset_password, and POST /users/login when login protection escalates to a challenge.
For JSON clients, fetch the challenge, solve it client-side with an ALTCHA-compatible library, and submit the resulting altcha string to the protected route.
For route-level requirements, see Help — When is ALTCHA required?.
MFA Login Contract#
When MFA is enabled and configured on an account, POST /users/login can return 202 Accepted with mfa_required, mfa_token, expiry fields, and mfa_methods instead of a session token.
Complete that challenge with POST /users/login/mfa by submitting mfa_token, code, and optional remember_device. A successful MFA verification returns the same auth-session JSON contract as a normal successful login.