Amba

Remote Config API

Full Admin + Client API for remote config — request shapes, response shapes, conditions, and ETag semantics.

This page is the reference for the remote-config HTTP surface. For conceptual overview see Remote Config; for stored segment and rollout conditions see Segment targeting.

Admin API

POST /v1/admin/projects/:projectId/config

Create or upsert a config key. ON CONFLICT (key) DO UPDATE — the same call sets a new value or updates an existing one.

Request body:

interface SetConfigInput {
  key: string;
  value: unknown;
  value_type?: 'string' | 'number' | 'boolean' | 'json'; // default 'string'
  description?: string | null;
  conditions?: Array<
    { segment_id: string; value: unknown } | { percentage: number; value: unknown }
  >;
}

Example:

{
  "key": "daily_limit",
  "value": 10,
  "value_type": "number",
  "description": "Maximum daily actions"
}

Response 201:

{
  "data": {
    "key": "daily_limit",
    "value": 10,
    "value_type": "number",
    "conditions": [],
    "default_value": 10,
    "version": 1,
    "is_active": true,
    "updated_at": "..."
  }
}

Behavior:

  • Updates are UPSERT — the same call creates or overwrites.
  • Every successful mutation bumps config_versions.version_hash (see ETag below).

GET /v1/admin/projects/:projectId/config

List every active config for the project, ordered by key.

{ "data": [ { "key": "daily_limit", ... }, { "key": "feature_paywall_v2", ... } ] }

PATCH /v1/admin/projects/:projectId/config/:key

Partial update. Only the fields you pass are changed. Passing value bumps the per-row version counter; passing only conditions or description does not (but still bumps config_versions.version_hash).

Request body:

{
  value?: unknown;
  description?: string | null;
  conditions?: Condition[];
}

404 if key doesn't exist.

DELETE /v1/admin/projects/:projectId/config/:key

Hard delete. Bumps config_versions.version_hash so clients re-fetch.

{ "data": { "deleted": true } }

Client API

GET /v1/client/config

Returns the config map with an ETag response header and Cache-Control: private, max-age=60 for a signed-in user (max-age=300 otherwise). For a signed-in user (a valid session token, or a server key with X-On-Behalf-Of) the route evaluates each key's conditions in order and returns the value of the first one that matches; every other caller, including one whose session token has expired, gets each key's default value. The SDK calls this route on every client.config.fetch(); in a browser, a repeat call within that window can return the browser's cached copy.

Request headers:

  • X-Api-Key: <amb_..._ck_...> (required)
  • Authorization: Bearer <session_token> (optional; the user the conditions are evaluated for)
  • If-None-Match: <etag> (optional, for cache revalidation)

Response:

{
  "data": {
    "daily_limit": 10,
    "feature_paywall_v2": false,
    "weekly_goal_count": 5
  }
}

The ETag header is "<version_hash>", followed by :<user_id>:<segments-hash> for a signed-in user, so it changes when a value or a condition changes and when the user joins or leaves a segment. When If-None-Match equals the current ETag, the server returns 304 Not Modified with no body.

Resolution

For a signed-in user the route walks each active key's conditions in order and takes the first one that applies (the user is in its segment_id, and inside its percentage when set); a key with no matching condition, and every key for a caller without a session, resolves to default_value. The resolved value is coerced to the key's value_type. Conditions and the default value stay on the server; the SDK sees the flat key → value map.

Error codes

CodeHTTPWhen
NOT_FOUND404PATCH / GET on an unknown key
SET_FAILED500UPSERT or config_versions update failed
UPDATE_FAILED500PATCH failed
DELETE_FAILED500DELETE failed

ETag + version hash

config_versions is a singleton row (id = 1). Its version_hash is a SHA-256 of every active config's (key, value, version, conditions) tuple. Any mutation recomputes it under an advisory lock so concurrent updates can't race each other.

A client that calls the route itself can keep the last map and its ETag, then revalidate:

  • Send If-None-Match: <saved etag>.
  • 200 → payload changed, replace the saved map and ETag.
  • 304 → payload unchanged, keep the saved map.

The SDK's client.config.fetch() sends no If-None-Match; each call returns the full map with a version read from the ETag.

Conditions JSON shape

Stored in remote_configs.conditions as a JSONB array. Each element is one of:

{ "segment_id": "seg_xxx", "value": <any> }
{ "percentage": 25, "value": <any> }

GET /v1/client/config coerces each returned value to the key's value_type (a "true" string to true, a JSON string to the parsed value) and passes through a value it can't coerce. Pass the same JSON shape for every condition and the default_value.

MCP tools

ToolDescription
amba_remote_configs_listList a project's config keys (values, types, conditions)
amba_remote_configs_createCreate / upsert a key with optional conditions
amba_remote_configs_updatePartially update a single key (only fields provided)
amba_remote_configs_deleteRemove a key

Next