{
  "openapi": "3.1.0",
  "info": {
    "title": "ContentFlow SDK API",
    "version": "v1",
    "summary": "The delivery API the ContentFlow SDKs call, for client developers.",
    "description": "The public API behind the ContentFlow SDKs (JavaScript, Swift, Kotlin, Flutter). Use it directly from a platform without an SDK, or to debug one.\n\n**Base URL:** `https://api.contentflow.click/sdk/v1`\n\n**Authentication:** send your workspace's publishable key as `X-CF-Key` (`X-API-Key` is accepted). Create one under Settings → API keys (type In-app): it looks like `<workspaceId>_cfpk…`, is shown once, and can be rotated or revoked. The older workspace-derived key, `<workspaceId>_test` (test) or `<workspaceId>_app` (live), still works unless the workspace switches it off; anyone who knows the workspace id can build it, so move apps to a created key. The workspace and environment come from the key. A tenant header is optional; with a created key it must name the key's workspace (`401 TENANT_KEY_MISMATCH` otherwise). Manually created `pk_test_…`/`pk_live_…` API keys are secret server/CI keys: they authenticate the same routes but must never be shipped in an app.\n\n**Abuse limits:** consent changes and new push devices are capped per user, and requests per network address; over a limit the answer is `429` with `Retry-After` (`code` `USER_RATE_LIMITED` or `IP_RATE_LIMITED`).\n\n**Environment header:** every authenticated response carries `X-CF-Env: live` or `X-CF-Env: test`, the environment the key resolved to. It is exposed to browsers through CORS (`Access-Control-Expose-Headers`).\n\n**Test environment:** a test key (including the one created for you on the portal Developers page) receives everything live devices receive plus content published to Test (\"Publish to Test\"); live devices never see test-only content. Devices and events from a test key are recorded as test traffic: a device id belongs to one environment, and analytics and block counters keep test traffic apart from live. Once sandboxes replace the in-workspace test environment, a test key of a workspace with a sandbox is served as the sandbox, and a test key of a workspace without one can only read (`409 SANDBOX_REQUIRED` on writes).\n\n**Device id:** generate a stable id per installation and send it as `X-CF-Device` (or `deviceId` in the body). Content reads work without one; device-scoped writes need it.\n\n**Envelope:** success is `{\"success\": true, \"data\": …}`; errors are `{\"success\": false, \"error\": \"…\", \"code\"?: \"…\"}`. A `200` can still carry `success: false`: always check it.\n\n**Consent:** analytics events are recorded only after the device identified with `consent: true`. Channel consent (push, SMS, …) is separate.\n\nChecked against the gateway source on 2026-10-05: 26 operations."
  },
  "servers": [
    {
      "url": "https://api.contentflow.click/sdk/v1",
      "description": "Production (use your _test key while exploring)"
    }
  ],
  "security": [
    {
      "publishableKey": []
    }
  ],
  "tags": [
    {
      "name": "Identity & lifecycle",
      "description": "Identify the installation and its user, sign out, erase."
    },
    {
      "name": "Content",
      "description": "Blocks your marketing team publishes."
    },
    {
      "name": "Localization",
      "description": "Translated strings and the workspace's locales."
    },
    {
      "name": "Campaigns",
      "description": "Campaign evaluation for a placement."
    },
    {
      "name": "Events",
      "description": "Analytics and identity events."
    },
    {
      "name": "Consent",
      "description": "Channel consent and consent presets."
    },
    {
      "name": "Push",
      "description": "Push token registration, topics and preferences."
    },
    {
      "name": "Location & geo",
      "description": "IP geolocation and device location uploads."
    },
    {
      "name": "Live updates",
      "description": "Server-Sent Events that signal content changes."
    },
    {
      "name": "Server-side (write key)",
      "description": "Build/CI tools. Never ship a write key in an app."
    }
  ],
  "paths": {
    "/blocks/{key}": {
      "get": {
        "tags": [
          "Content"
        ],
        "operationId": "getBlock",
        "summary": "Get one block",
        "description": "Resolves a single block key for this device. `visible: false` means the block exists but nothing is currently published for this device.\n\n**SDK:** `fetchBlock(key)`",
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "home_banner"
          },
          {
            "$ref": "#/components/parameters/Device"
          },
          {
            "$ref": "#/components/parameters/Locale"
          },
          {
            "$ref": "#/components/parameters/AcceptLanguage"
          },
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          },
          {
            "$ref": "#/components/parameters/Capabilities"
          },
          {
            "$ref": "#/components/parameters/SessionId"
          }
        ],
        "responses": {
          "200": {
            "description": "The block, or `{key, visible: false, version}`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Success envelope.",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "oneOf": [
                        {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/Block"
                            },
                            {
                              "type": "object",
                              "properties": {
                                "visible": {
                                  "const": true
                                },
                                "instances": {
                                  "type": "array",
                                  "items": {
                                    "type": "object"
                                  }
                                }
                              }
                            }
                          ]
                        },
                        {
                          "type": "object",
                          "properties": {
                            "key": {
                              "type": "string"
                            },
                            "visible": {
                              "const": false
                            },
                            "version": {
                              "type": "string"
                            }
                          }
                        }
                      ]
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "key": "home_banner",
                    "name": "Home banner",
                    "screen": "home",
                    "instanceId": "inst_7c2f",
                    "segment": "vip",
                    "version": 4,
                    "values": {
                      "#title": "أهلاً بك من جديد",
                      "#image": "https://api.contentflow.click/sdk/v1/content/9b2d",
                      "#cta": "Open offer"
                    },
                    "fields": [
                      {
                        "tag": "#title",
                        "type": "text"
                      },
                      {
                        "tag": "#image",
                        "type": "image"
                      },
                      {
                        "tag": "#cta",
                        "type": "text"
                      }
                    ],
                    "visible": true,
                    "instances": []
                  }
                }
              }
            },
            "headers": {
              "ETag": {
                "description": "Weak validator for this payload. Send it back as `If-None-Match` to get `304`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No block with this key in the workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "unknown block"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/GatewayUnavailable"
          },
          "503": {
            "$ref": "#/components/responses/KeyValidationUnavailable"
          }
        }
      }
    },
    "/campaigns": {
      "post": {
        "tags": [
          "Campaigns"
        ],
        "operationId": "getCampaigns",
        "summary": "Get campaigns for a placement",
        "description": "Evaluates active campaigns for a placement and returns them in descending priority. The workspace and environment come from your key; everything else in the body is evaluation context. The locale is read from `Accept-Language` on this route.\n\n**SDK:** `getCampaigns(placement)`\n\n**When campaigns are switched off for the workspace** (`config.features.campaigns: false` on `/sync`), this route answers `200` with an empty list: `{\"success\": true, \"data\": [], \"metadata\": {\"campaignCount\": 0, \"feature\": \"campaigns\", \"reason\": \"feature_disabled\"}}`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Device"
          },
          {
            "$ref": "#/components/parameters/AcceptLanguage"
          },
          {
            "$ref": "#/components/parameters/SessionId"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "placementId": {
                    "type": "string"
                  },
                  "userId": {
                    "type": "string"
                  },
                  "deviceId": {
                    "type": "string"
                  },
                  "segments": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "deprecated": true,
                    "description": "Ignored. Segment membership is resolved on the server from this device's last identify call, never taken from the request."
                  },
                  "attributes": {
                    "type": "object",
                    "additionalProperties": true
                  },
                  "deviceInfo": {
                    "type": "object",
                    "additionalProperties": true
                  },
                  "location": {
                    "type": "object",
                    "additionalProperties": true
                  },
                  "context": {
                    "type": "object",
                    "additionalProperties": true
                  }
                }
              },
              "example": {
                "placementId": "home_hero",
                "userId": "cust_1042",
                "deviceId": "3f6c1a2e-8a4b-4f0e-9c1d-2b7e5d9a1c44",
                "segments": [
                  "vip"
                ],
                "attributes": {
                  "language": "ar"
                },
                "deviceInfo": {
                  "platform": "ios"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Matching campaigns.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      }
                    },
                    "metadata": {
                      "type": "object",
                      "properties": {
                        "tenantId": {
                          "type": "string"
                        },
                        "userId": {
                          "type": "string"
                        },
                        "campaignCount": {
                          "type": "integer"
                        },
                        "feature": {
                          "type": "string",
                          "description": "Present only when the workspace has switched this feature off."
                        },
                        "reason": {
                          "type": "string",
                          "enum": [
                            "feature_disabled"
                          ],
                          "description": "Present only when the workspace has switched this feature off."
                        }
                      }
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": [],
                  "metadata": {
                    "tenantId": "ws_1a2b",
                    "userId": "cust_1042",
                    "campaignCount": 0
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body is not a JSON object.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "Campaign context must be a JSON object"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/GatewayUnavailable"
          },
          "503": {
            "$ref": "#/components/responses/KeyValidationUnavailable"
          }
        }
      }
    },
    "/cards/register": {
      "post": {
        "tags": [
          "Server-side (write key)"
        ],
        "operationId": "cardsRegister",
        "summary": "Register one block type",
        "description": "Creates or updates a single block definition. `key` must match `^[a-z0-9_]+$`.\n\n**Server/CI only: never ship a write key in an app.** Swagger UI on this site refuses to send write-key requests; run the `curl` from your build or CI.\n\n**SDK:** the SDK helpers (`registerBlock`, `syncBlocks`) must not be used in 2.2.1: they nest the definition under `definition`, which this route does not read, and go through `/cards/sync`, which archives every other block type. Call this route directly.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BlockDefinition"
              },
              "example": {
                "key": "promo_card",
                "name": "Promo card",
                "fields": [
                  {
                    "tag": "#title",
                    "type": "text",
                    "label": "Title"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Registered.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Success envelope.",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "registered": {
                          "type": "string"
                        },
                        "definitionCreated": {
                          "type": "boolean"
                        },
                        "instanceId": {
                          "type": "string"
                        },
                        "instanceCreated": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "registered": "promo_card",
                    "definitionCreated": true,
                    "instanceId": "inst_1e0a",
                    "instanceCreated": true
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid key or fields.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "key is required and must match /^[a-z0-9_]+$/"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid write key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "invalid write key"
                }
              }
            }
          },
          "502": {
            "$ref": "#/components/responses/GatewayUnavailable"
          }
        },
        "security": [
          {
            "writeKey": []
          }
        ]
      }
    },
    "/cards/sync": {
      "post": {
        "tags": [
          "Server-side (write key)"
        ],
        "operationId": "cardsSync",
        "summary": "Declare your block types (cards as code)",
        "description": "Declares the COMPLETE set of block types. Blocks in the manifest are created or updated, and every currently active block type the manifest does not name is archived, whatever `prune` says. Send your full set on every run, or use `POST /cards/register` to upsert one type without touching the others. Each manifest entry is a flat definition (`key`, `name`, `screen`, `fields`, optional `instances`), not wrapped in a `definition` object. `prune: true` is only needed (and only meaningful) with an empty `blocks` array, which then archives everything; an empty manifest without it is refused with 400. There is no dry-run mode. Keys carrying sponsored content are never archived and are listed in `archiveRefused`.\n\n**Server/CI only: never ship a write key in an app.** Swagger UI on this site refuses to send write-key requests; run the `curl` from your build or CI.\n\n**SDK:** the SDK helpers (`registerBlock`, `syncBlocks`) must not be used in 2.2.1: they nest the definition under `definition`, which this route does not read, and go through `/cards/sync`, which archives every other block type. Call this route directly.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "blocks": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/BlockDefinition"
                    }
                  },
                  "cards": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/BlockDefinition"
                    },
                    "description": "Alias of `blocks`."
                  },
                  "prune": {
                    "type": "boolean",
                    "description": "Required to send an empty `blocks` array (archives every block type). Has no effect on a non-empty manifest, which always archives unnamed types."
                  }
                }
              },
              "example": {
                "blocks": [
                  {
                    "key": "promo_card",
                    "name": "Promo card",
                    "screen": "home",
                    "fields": [
                      {
                        "tag": "#title",
                        "type": "text",
                        "label": "Title"
                      },
                      {
                        "tag": "#image",
                        "type": "image",
                        "label": "Image"
                      }
                    ]
                  }
                ],
                "prune": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Synced.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Success envelope.",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "active": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "created": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "archived": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "archiveRefused": {
                          "type": "array",
                          "items": {}
                        },
                        "instancesSeeded": {
                          "type": "integer"
                        },
                        "errors": {
                          "type": "array",
                          "items": {}
                        }
                      }
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "active": [
                      "promo_card"
                    ],
                    "created": [
                      "promo_card"
                    ],
                    "archived": [],
                    "archiveRefused": [],
                    "instancesSeeded": 0,
                    "errors": []
                  }
                }
              }
            }
          },
          "400": {
            "description": "`blocks` is not an array, or an empty manifest without `prune: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "blocks (or cards) must be an array"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid write key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "invalid write key"
                }
              }
            }
          },
          "502": {
            "$ref": "#/components/responses/GatewayUnavailable"
          }
        },
        "security": [
          {
            "writeKey": []
          }
        ]
      }
    },
    "/consent": {
      "post": {
        "tags": [
          "Consent"
        ],
        "operationId": "setChannelConsent",
        "summary": "Set channel consent",
        "description": "Records which channels this person agreed to be contacted on (`push`, `sms`, `whatsapp`, `email`, `locationTracking`, `marketing`). Independent of analytics consent, which is set through `/identify`. Flags must be nested under `channels`.\n\n**SDK:** `setChannelConsent()` Ownership codes (`DEVICE_REQUIRED`, `DEVICE_NOT_IDENTIFIED`, `USER_MISMATCH`) apply once the consent ownership fix (#870) is deployed.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Device"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "channels"
                ],
                "properties": {
                  "deviceId": {
                    "type": "string"
                  },
                  "username": {
                    "type": "string",
                    "description": "Your user id. `userId` is accepted as an alias."
                  },
                  "channels": {
                    "type": "object",
                    "properties": {
                      "push": {
                        "type": "boolean"
                      },
                      "sms": {
                        "type": "boolean"
                      },
                      "whatsapp": {
                        "type": "boolean"
                      },
                      "email": {
                        "type": "boolean"
                      },
                      "locationTracking": {
                        "type": "boolean"
                      },
                      "marketing": {
                        "type": "boolean"
                      }
                    }
                  }
                }
              },
              "example": {
                "deviceId": "3f6c1a2e-8a4b-4f0e-9c1d-2b7e5d9a1c44",
                "username": "cust_1042",
                "channels": {
                  "push": true,
                  "sms": false
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stored.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Success envelope.",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "deviceId": {
                          "type": "string"
                        },
                        "username": {
                          "type": "string"
                        },
                        "channels": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "boolean"
                          }
                        },
                        "results": {
                          "type": "object",
                          "properties": {
                            "device": {
                              "$ref": "#/components/schemas/OperationResult"
                            },
                            "consent": {
                              "$ref": "#/components/schemas/OperationResult"
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "deviceId": "3f6c1a2e-8a4b-4f0e-9c1d-2b7e5d9a1c44",
                    "username": "cust_1042",
                    "channels": {
                      "push": true,
                      "sms": false
                    },
                    "results": {
                      "device": {
                        "applied": true
                      },
                      "consent": {
                        "applied": true
                      }
                    }
                  }
                }
              }
            }
          },
          "207": {
            "description": "The device updated but the consent record did not store. Retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartialFailure"
                },
                "example": {
                  "success": false,
                  "error": "failed to store channel consent"
                }
              }
            }
          },
          "400": {
            "description": "No `channels` object, no known channel, or flags at the top level. Or a user is named without a device (`DEVICE_REQUIRED`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "channels_required": {
                    "value": {
                      "success": false,
                      "error": "channels object is required, e.g. {\"deviceId\":\"…\",\"channels\":{\"push\":true}}",
                      "code": "CHANNELS_REQUIRED"
                    }
                  },
                  "device_required": {
                    "summary": "User named, but no device",
                    "value": {
                      "success": false,
                      "code": "DEVICE_REQUIRED",
                      "error": "X-CF-Device header is required"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The body names a user (`username`/`userId`) this device is not signed in as. Call `identify()` with that user first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "user_id_device_mismatch": {
                    "value": {
                      "success": false,
                      "error": "...",
                      "code": "USER_ID_DEVICE_MISMATCH"
                    }
                  },
                  "device_not_identified": {
                    "summary": "Device not signed in as any user",
                    "value": {
                      "success": false,
                      "code": "DEVICE_NOT_IDENTIFIED",
                      "error": "this device is not signed in as a user; call identify() with this userId first"
                    }
                  },
                  "user_mismatch": {
                    "summary": "Device signed in as a different user",
                    "value": {
                      "success": false,
                      "code": "USER_MISMATCH",
                      "error": "this device is signed in as a different user"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "Device belongs to the other environment. Or, once the test environment is retired, a test key of a workspace with no sandbox cannot write, code `SANDBOX_REQUIRED`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "environmentMismatch": {
                    "summary": "Device or profile in the other environment",
                    "value": {
                      "success": false,
                      "error": "device belongs to a different environment",
                      "code": "DEVICE_ENVIRONMENT_MISMATCH"
                    }
                  },
                  "sandboxRequired": {
                    "summary": "Test key, workspace without a sandbox",
                    "value": {
                      "success": false,
                      "error": "This workspace has no sandbox: test keys can only read live content. Create a sandbox in Settings > Workspace",
                      "code": "SANDBOX_REQUIRED"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/GatewayUnavailable"
          },
          "503": {
            "$ref": "#/components/responses/KeyValidationUnavailable"
          }
        }
      }
    },
    "/consent/config/{preset}": {
      "get": {
        "tags": [
          "Consent"
        ],
        "operationId": "getConsentConfig",
        "summary": "Get a consent preset",
        "description": "What a consent screen for a regulated Saudi industry should offer: required and optional items, default topics, and the legal framework. Reading a preset never changes anyone's consent.\n\n**SDK:** `getConsentConfig(preset)`",
        "parameters": [
          {
            "name": "preset",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "banking_ksa",
                "insurance_ksa",
                "healthcare_ksa",
                "travel_ksa",
                "retail_ksa",
                "food_delivery_ksa",
                "fintech_ksa"
              ]
            },
            "example": "banking_ksa"
          }
        ],
        "responses": {
          "200": {
            "description": "The preset.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Success envelope.",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "additionalProperties": true
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "preset": "banking_ksa",
                    "industry": "banking",
                    "region": "SA",
                    "required": [
                      "channels.push"
                    ],
                    "optional": [
                      "marketing.promotional"
                    ],
                    "defaultTopics": [
                      "security_alerts"
                    ],
                    "availableTopics": [
                      {
                        "id": "security_alerts",
                        "name": "Security alerts",
                        "nameAr": "تنبيهات الأمان",
                        "required": true
                      }
                    ],
                    "legalFramework": "SAMA + PDPL"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Unknown preset.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "availablePresets": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "The preset ids this workspace can use. Omitted when the consent service does not list them."
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "success": false,
                  "error": "unknown consent preset",
                  "code": "UNKNOWN_CONSENT_PRESET",
                  "availablePresets": [
                    "banking_ksa"
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/GatewayUnavailable"
          },
          "503": {
            "$ref": "#/components/responses/KeyValidationUnavailable"
          }
        }
      }
    },
    "/consent/full": {
      "post": {
        "tags": [
          "Consent"
        ],
        "operationId": "setFullConsent",
        "summary": "Set channels, marketing, topics and preferences at once",
        "description": "Saves a complete consent screen for the signed-in user: channels, marketing categories, location, behavioral analytics, industry preset topics and push preferences. Needs a user: pass `userId` or identify the device first.\n\n**SDK:** `updateConsent()`, `requestConsent()` Ownership codes (`DEVICE_REQUIRED`, `DEVICE_NOT_IDENTIFIED`, `USER_MISMATCH`) apply once the consent ownership fix (#870) is deployed.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Device"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "deviceId": {
                    "type": "string"
                  },
                  "userId": {
                    "type": "string"
                  },
                  "channels": {
                    "type": "object",
                    "properties": {
                      "push": {
                        "type": "boolean"
                      },
                      "sms": {
                        "type": "boolean"
                      },
                      "whatsapp": {
                        "type": "boolean"
                      },
                      "email": {
                        "type": "boolean"
                      },
                      "inApp": {
                        "type": "boolean"
                      },
                      "locationTracking": {
                        "type": "boolean"
                      },
                      "marketing": {
                        "type": "boolean"
                      }
                    }
                  },
                  "marketing": {
                    "type": "object",
                    "properties": {
                      "promotional": {
                        "type": "boolean"
                      },
                      "transactional": {
                        "type": "boolean"
                      },
                      "serviceUpdates": {
                        "type": "boolean"
                      },
                      "thirdPartyOffers": {
                        "type": "boolean"
                      }
                    }
                  },
                  "location": {
                    "type": "boolean"
                  },
                  "behavioral": {
                    "type": "boolean"
                  },
                  "version": {
                    "type": "string"
                  },
                  "legalBasis": {
                    "type": "string"
                  },
                  "pdplOptOut": {
                    "type": "boolean"
                  },
                  "preset": {
                    "type": "string"
                  },
                  "topics": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "preferences": {
                    "$ref": "#/components/schemas/PushPreferences"
                  }
                }
              },
              "example": {
                "deviceId": "3f6c1a2e-8a4b-4f0e-9c1d-2b7e5d9a1c44",
                "userId": "cust_1042",
                "preset": "banking_ksa",
                "channels": {
                  "push": true,
                  "email": true
                },
                "marketing": {
                  "promotional": false,
                  "transactional": true
                },
                "topics": [
                  "security_alerts"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stored. Echoes what was stored.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Success envelope.",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "additionalProperties": true
                    }
                  }
                }
              }
            }
          },
          "207": {
            "description": "Consent stored, but push preferences did not. Retry the preferences only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartialFailure"
                },
                "example": {
                  "success": false,
                  "error": "failed to store push preferences"
                }
              }
            }
          },
          "400": {
            "description": "No device id, no user, or an invalid topic list. Or a `userId` is named without a device (`DEVICE_REQUIRED`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "user_required": {
                    "value": {
                      "success": false,
                      "error": "userId required (provide in body or identify the device first)"
                    }
                  },
                  "device_required": {
                    "summary": "User named, but no device",
                    "value": {
                      "success": false,
                      "code": "DEVICE_REQUIRED",
                      "error": "X-CF-Device header is required"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The body's `userId` is not the user this device is signed in as. Call `identify()` with that user first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "device_not_identified": {
                    "summary": "Device not signed in as any user",
                    "value": {
                      "success": false,
                      "code": "DEVICE_NOT_IDENTIFIED",
                      "error": "this device is not signed in as a user; call identify() with this userId first"
                    }
                  },
                  "user_mismatch": {
                    "summary": "Device signed in as a different user",
                    "value": {
                      "success": false,
                      "code": "USER_MISMATCH",
                      "error": "this device is signed in as a different user"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The profile belongs to the other environment: a test key cannot change a live customer's consent, code `DEVICE_ENVIRONMENT_MISMATCH`. Nothing is written. Or, once the test environment is retired, a test key of a workspace with no sandbox cannot write, code `SANDBOX_REQUIRED`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "environmentMismatch": {
                    "summary": "Device or profile in the other environment",
                    "value": {
                      "success": false,
                      "error": "profile belongs to a different environment",
                      "code": "DEVICE_ENVIRONMENT_MISMATCH"
                    }
                  },
                  "sandboxRequired": {
                    "summary": "Test key, workspace without a sandbox",
                    "value": {
                      "success": false,
                      "error": "This workspace has no sandbox: test keys can only read live content. Create a sandbox in Settings > Workspace",
                      "code": "SANDBOX_REQUIRED"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "The consent record could not be stored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "failed to update consent"
                }
              }
            }
          },
          "502": {
            "$ref": "#/components/responses/GatewayUnavailable"
          },
          "503": {
            "$ref": "#/components/responses/KeyValidationUnavailable"
          }
        }
      }
    },
    "/content/{id}": {
      "get": {
        "tags": [
          "Content"
        ],
        "operationId": "getContent",
        "summary": "Open an uploaded media file",
        "description": "Redirects to a short-lived URL for an uploaded image or video. Block image values point here; you rarely call it directly.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "302": {
            "description": "Redirect to the file.",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "`id` is not a plain identifier (letters, digits, `_` and `-`, at most 128 characters).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "Invalid content id"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No content with this id in the workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "Content not found"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/GatewayUnavailable"
          },
          "503": {
            "$ref": "#/components/responses/KeyValidationUnavailable"
          }
        }
      }
    },
    "/events": {
      "post": {
        "tags": [
          "Events"
        ],
        "operationId": "trackEvents",
        "summary": "Send a batch of analytics events",
        "description": "Queues up to 100 events. Events are recorded only for a device that has identified with analytics consent granted; otherwise the whole batch is acknowledged as dropped with a reason. `202` means queued at the gateway, not yet durably stored. A timeout cannot prove delivery; don't blindly retry.\n\n**SDK:** `track()`, `trackImpression()`, `trackTap()`, `trackConversion()`, `flush()`",
        "parameters": [
          {
            "$ref": "#/components/parameters/Device"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "events"
                ],
                "properties": {
                  "deviceId": {
                    "type": "string"
                  },
                  "userId": {
                    "type": "string"
                  },
                  "category": {
                    "type": "string"
                  },
                  "events": {
                    "type": "array",
                    "maxItems": 100,
                    "items": {
                      "$ref": "#/components/schemas/SdkEvent"
                    }
                  },
                  "context": {
                    "type": "object",
                    "properties": {
                      "sessionId": {
                        "type": "string"
                      },
                      "platform": {
                        "type": "string"
                      },
                      "osVersion": {
                        "type": "string"
                      },
                      "appVersion": {
                        "type": "string"
                      },
                      "deviceModel": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "deviceId": "3f6c1a2e-8a4b-4f0e-9c1d-2b7e5d9a1c44",
                "events": [
                  {
                    "type": "impression",
                    "blockKey": "home_banner",
                    "instanceId": "inst_7c2f",
                    "screen": "home"
                  },
                  {
                    "type": "commerce:purchase",
                    "custom": {
                      "amount": 99,
                      "currency": "SAR"
                    }
                  }
                ],
                "context": {
                  "sessionId": "s_91",
                  "platform": "ios",
                  "appVersion": "5.2.0"
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Acknowledged. Check `accepted`/`dropped`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Success envelope.",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "accepted": {
                          "type": "integer"
                        },
                        "dropped": {
                          "type": "integer"
                        },
                        "reason": {
                          "type": "string"
                        },
                        "dropReasons": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "integer"
                          },
                          "description": "e.g. `consent_not_granted`, `device_not_identified`, `invalid_string_signal`, `sensor_not_enabled` (a string signal whose key is not an enabled sensor in the portal)."
                        }
                      }
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "accepted": 2,
                    "dropped": 0,
                    "dropReasons": {}
                  }
                }
              }
            }
          },
          "400": {
            "description": "Batch rejected before anything was queued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventInputError"
                },
                "example": {
                  "success": false,
                  "code": "ENDPOINT_MISMATCH",
                  "error": "Send identity events to /track-event with username and device identity; no events were queued",
                  "eventIndex": 0,
                  "data": {
                    "accepted": 0
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The body names a `userId` this device is not signed in as. Events are always attributed to the device's own sign-in, so the whole batch is refused and nothing is queued. Omit `userId`, or call `identify()` with that user first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "device_not_identified": {
                    "summary": "Device not signed in as any user",
                    "value": {
                      "success": false,
                      "code": "DEVICE_NOT_IDENTIFIED",
                      "error": "this device is not signed in as a user; call identify() with this userId first"
                    }
                  },
                  "user_mismatch": {
                    "summary": "Device signed in as a different user",
                    "value": {
                      "success": false,
                      "code": "USER_MISMATCH",
                      "error": "this device is signed in as a different user"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/SandboxRequired"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "The event queue is unavailable; nothing was queued. Also answered, with `code` `UPSTREAM_UNAVAILABLE` and `Retry-After`, when a created publishable key could not be validated; retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "Event queue unavailable; no events were queued"
                }
              }
            }
          }
        }
      }
    },
    "/geo/ip": {
      "get": {
        "tags": [
          "Location & geo"
        ],
        "operationId": "getIPGeolocation",
        "summary": "Coarse location from the caller's IP",
        "description": "Country, region, city, approximate coordinates and time zone for the request's IP address, resolved on the gateway from a local MaxMind GeoLite2-City database. The IP address is never sent to a third party. No permission prompt and no device id needed.\n\n**Unknown location.** A private, loopback or reserved address (`isPrivate: true`), an address the database does not cover, or a gateway without the database installed all answer `200` with empty strings, `0` for the numbers and no `postalCode`. Check `country` before using the result.\n\n**Not provided:** ISP, organisation, ASN, connection type and proxy/hosting detection are not in GeoLite2-City, so the `isp`, `org`, `asn`, `connectionType`, `isProxy` and `isHosting` fields of the SDK models are never set.\n\nThis product includes GeoLite2 data created by MaxMind, available from https://www.maxmind.com.\n\n**SDK:** `getIPGeolocation()`",
        "responses": {
          "200": {
            "description": "Location.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Success envelope.",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "ip",
                        "country",
                        "countryName",
                        "region",
                        "regionName",
                        "city",
                        "lat",
                        "lng",
                        "accuracyRadius",
                        "timezone",
                        "utcOffset",
                        "isPrivate"
                      ],
                      "properties": {
                        "ip": {
                          "type": "string",
                          "description": "The address that was looked up (the first X-Forwarded-For hop)."
                        },
                        "country": {
                          "type": "string",
                          "description": "ISO 3166-1 alpha-2 country code, e.g. `SA`. Empty when unknown."
                        },
                        "countryName": {
                          "type": "string",
                          "description": "English country name. Empty when unknown."
                        },
                        "region": {
                          "type": "string",
                          "description": "ISO 3166-2 code of the top-level subdivision, without the country prefix (`01` for Riyadh Region). Empty when unknown."
                        },
                        "regionName": {
                          "type": "string",
                          "description": "English name of the top-level subdivision. Empty when unknown."
                        },
                        "city": {
                          "type": "string",
                          "description": "English city name. Empty when unknown."
                        },
                        "postalCode": {
                          "type": "string",
                          "description": "Postal code, when the database has one for the address. Omitted otherwise."
                        },
                        "lat": {
                          "type": "number",
                          "description": "Approximate latitude; see `accuracyRadius`. 0 when unknown."
                        },
                        "lng": {
                          "type": "number",
                          "description": "Approximate longitude; see `accuracyRadius`. 0 when unknown."
                        },
                        "accuracyRadius": {
                          "type": "integer",
                          "description": "Radius in kilometres around `lat`/`lng` within which the address is likely to be. IP locations are coarse: often tens to hundreds of km. 0 when unknown.",
                          "minimum": 0
                        },
                        "timezone": {
                          "type": "string",
                          "description": "IANA time zone, e.g. `Asia/Riyadh`. Empty when unknown."
                        },
                        "utcOffset": {
                          "type": "number",
                          "description": "The time zone's current offset from UTC in hours, daylight saving included (3 for Asia/Riyadh, 5.5 for Asia/Kolkata). 0 when `timezone` is empty."
                        },
                        "isPrivate": {
                          "type": "boolean",
                          "description": "`true` for a private, loopback or reserved address, which is never looked up."
                        }
                      }
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "ip": "203.0.113.7",
                    "country": "SA",
                    "countryName": "Saudi Arabia",
                    "region": "01",
                    "regionName": "Riyadh Region",
                    "city": "Riyadh",
                    "lat": 24.6905,
                    "lng": 46.7096,
                    "accuracyRadius": 20,
                    "timezone": "Asia/Riyadh",
                    "utcOffset": 3,
                    "isPrivate": false
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/KeyValidationUnavailable"
          }
        }
      }
    },
    "/geofences": {
      "get": {
        "tags": [
          "Location & geo"
        ],
        "operationId": "getGeofences",
        "summary": "Get the geofences this device should monitor",
        "description": "Returns the workspace's geofences for this device to monitor with the operating system (iOS region monitoring, Android `GeofencingClient`). The device then reports each transition in `POST /location` `geofenceEvents[]`, and the server applies it through the same state as server-side evaluation, so a stay fires once whichever side sees it first.\n\n**Which fences.** Only active fences of this workspace that target this device's user or segments, and only when the user has granted location consent (`setChannelConsent({ locationTracking: true })` or `requestConsent({ location: true })`) on top of analytics consent. Otherwise `fences` is empty. Nearest first when you send `lat` and `lng`, otherwise by fence priority; at most `max`; `truncated` says whether more were left out.\n\n**Polygons** are sent as their smallest enclosing circle with `exact: \"polygon\"`. Monitor the circle; the server confirms an `enter` or `dwell` against the polygon itself with the position you report.\n\n**Triggers** say which transitions fire on the server. Monitor and report `enter` and `exit` for every fence even when a flag is false, because the server needs both to track the stay; report `dwell` only when `triggers.dwell` is true, after `dwellMs` inside.\n\n**Refreshing.** Fetch again after `refreshAfterSec`, after the device moves far (re-select the nearest fences), and after consent changes. Send the `ETag` back as `If-None-Match`; an unchanged list answers `304`. The ETag is computed over the list for the position in this request, so after moving, a `304` means the nearest set for the new position is the one you have. The position is coarse (rounded to about 100 m), used only for ordering, and never stored.\n\n**When location is switched off for the workspace** (`config.features.location: false` on `/sync`), this route answers `200` with an empty `fences` list. Stop monitoring.\n\n**Web:** server-side only; browsers have no geofencing API.\n\n**SDK:** on-device geofencing (SDK 2.3.0)",
        "parameters": [
          {
            "$ref": "#/components/parameters/DeviceRequired"
          },
          {
            "name": "lat",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number",
              "minimum": -90,
              "maximum": 90
            },
            "example": 24.71,
            "description": "Coarse device latitude, for nearest-first ordering only. Send with `lng`."
          },
          {
            "name": "lng",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number",
              "minimum": -180,
              "maximum": 180
            },
            "example": 46.68,
            "description": "Coarse device longitude. Send with `lat`."
          },
          {
            "name": "max",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            },
            "example": 20,
            "description": "At most this many fences. iOS monitors up to 20 regions per app and Android up to 100; leave room for your own."
          },
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          }
        ],
        "responses": {
          "200": {
            "description": "The fences to monitor (possibly none).",
            "headers": {
              "ETag": {
                "description": "Weak validator for this list. Send it back as `If-None-Match` to get `304`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Success envelope.",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "fences",
                        "maxRegions",
                        "refreshAfterSec",
                        "truncated",
                        "version"
                      ],
                      "properties": {
                        "fences": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "required": [
                              "id",
                              "region",
                              "exact",
                              "triggers"
                            ],
                            "properties": {
                              "id": {
                                "type": "string",
                                "description": "The fence id; report it as `fenceId`."
                              },
                              "region": {
                                "type": "object",
                                "required": [
                                  "lat",
                                  "lng",
                                  "radiusMeters"
                                ],
                                "description": "The circle to monitor: the fence itself, or a polygon's smallest enclosing circle.",
                                "properties": {
                                  "lat": {
                                    "type": "number"
                                  },
                                  "lng": {
                                    "type": "number"
                                  },
                                  "radiusMeters": {
                                    "type": "number",
                                    "description": "Radius in metres."
                                  }
                                }
                              },
                              "exact": {
                                "type": "string",
                                "enum": [
                                  "circle",
                                  "polygon"
                                ],
                                "description": "The fence's real shape. `polygon`: the server confirms an enter or dwell against the polygon with the reported position."
                              },
                              "triggers": {
                                "type": "object",
                                "required": [
                                  "enter",
                                  "exit",
                                  "dwell",
                                  "dwellMs"
                                ],
                                "description": "Which transitions fire on the server. Report enter and exit regardless.",
                                "properties": {
                                  "enter": {
                                    "type": "boolean"
                                  },
                                  "exit": {
                                    "type": "boolean"
                                  },
                                  "dwell": {
                                    "type": "boolean"
                                  },
                                  "dwellMs": {
                                    "type": "integer",
                                    "description": "Time inside before a dwell, in milliseconds."
                                  }
                                }
                              }
                            }
                          },
                          "description": "Nearest first when a position was sent, else by priority. Empty when there is nothing to monitor."
                        },
                        "maxRegions": {
                          "type": "integer",
                          "description": "The cap applied (`max`, or 20)."
                        },
                        "refreshAfterSec": {
                          "type": "integer",
                          "description": "Fetch again after this many seconds."
                        },
                        "truncated": {
                          "type": "boolean",
                          "description": "`true` when more fences meant for this device exist than were returned (the list was cut at `max`). No count is given. When `false`, you have every fence; there is no need to ask for `max + 1`."
                        },
                        "version": {
                          "type": "string",
                          "description": "Identifies this answer; the same value is the `ETag`. It covers the list returned for this request's (rounded) position, including `truncated`, so a `304` means the nearest set for the position you just sent is the one you have."
                        }
                      }
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "fences": [
                      {
                        "id": "store-12",
                        "region": {
                          "lat": 24.7136,
                          "lng": 46.6753,
                          "radiusMeters": 150
                        },
                        "exact": "circle",
                        "triggers": {
                          "enter": true,
                          "exit": true,
                          "dwell": false,
                          "dwellMs": 60000
                        }
                      },
                      {
                        "id": "kafd-campus",
                        "region": {
                          "lat": 24.7629,
                          "lng": 46.6407,
                          "radiusMeters": 812
                        },
                        "exact": "polygon",
                        "triggers": {
                          "enter": true,
                          "exit": false,
                          "dwell": true,
                          "dwellMs": 300000
                        }
                      }
                    ],
                    "maxRegions": 20,
                    "refreshAfterSec": 3600,
                    "truncated": true,
                    "version": "g1-5d41402abc4b2a76b9719d911017c592"
                  }
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "400": {
            "description": "No `X-CF-Device` header, or an invalid `lat`/`lng`/`max`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "max must be an integer from 1 to 100"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/GatewayUnavailable"
          },
          "503": {
            "description": "The device state could not be read. Keep monitoring the fences you have and retry after `Retry-After`. Also answered, with `code` `UPSTREAM_UNAVAILABLE` and `Retry-After`, when a created publishable key could not be validated; retry.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "device state temporarily unavailable",
                  "code": "UPSTREAM_UNAVAILABLE",
                  "retryable": true
                }
              }
            }
          }
        }
      }
    },
    "/identify": {
      "post": {
        "tags": [
          "Identity & lifecycle"
        ],
        "operationId": "identify",
        "summary": "Identify a device (and optionally its user)",
        "description": "Registers or updates this installation, links it to your signed-in user, records analytics consent and traits, and returns the device's segments. Call it on app start and whenever the signed-in user, consent or traits change. Unknown body keys are listed in `data.ignoredKeys` rather than rejected. Trait keys that were not stored (a key outside letters, digits and `_` up to 64 characters, more than 50 traits in one call, or a value over 4 KB) are listed in `data.ignoredTraitKeys`. Traits, segments and the user link are only written while analytics consent is granted. After an erasure (`DELETE /identify`) the person's profile is not recreated until consent is granted again: an identify that sends `consent: true` after the erasure. Until then `data.results.identity` is `{applied: false, status: \"erased\"}`, no user link or traits are stored, and only the baseline segments are returned. While an erasure of this device is still incomplete, identify answers `409` with code `ERASURE_PENDING`: retry `DELETE /identify` first.\n\n**SDK:** `identify()`, `setUserId()`, `setConsent()`",
        "parameters": [
          {
            "$ref": "#/components/parameters/Device"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/IdentifyRequest"
              },
              "example": {
                "deviceId": "3f6c1a2e-8a4b-4f0e-9c1d-2b7e5d9a1c44",
                "userId": "cust_1042",
                "consent": true,
                "traits": {
                  "plan": "gold",
                  "city": "Riyadh"
                },
                "platform": "ios"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Identified.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Success envelope.",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "deviceId": {
                          "type": "string"
                        },
                        "userId": {
                          "type": "string"
                        },
                        "segments": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "segmentIds": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "Ids of the evaluated segments, alongside their names in segments. Omitted when the segmentation service returned none."
                        },
                        "consent": {
                          "type": "boolean"
                        },
                        "ignoredKeys": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "ignoredTraitKeys": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "maxItems": 50,
                          "description": "Trait keys that were not stored: an invalid key (only letters, digits and `_`, at most 64 characters), beyond the 50th trait in one call, or a value over 4 KB. Omitted when every trait was stored."
                        },
                        "results": {
                          "type": "object",
                          "properties": {
                            "device": {
                              "$ref": "#/components/schemas/OperationResult"
                            },
                            "identity": {
                              "$ref": "#/components/schemas/OperationResult"
                            }
                          }
                        },
                        "pushRegistration": {
                          "$ref": "#/components/schemas/OperationResult"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "deviceId": "3f6c1a2e-8a4b-4f0e-9c1d-2b7e5d9a1c44",
                    "userId": "cust_1042",
                    "segments": [
                      "vip"
                    ],
                    "consent": true,
                    "ignoredKeys": [],
                    "results": {
                      "device": {
                        "applied": true
                      },
                      "identity": {
                        "applied": true,
                        "status": "identified"
                      }
                    }
                  }
                }
              }
            }
          },
          "207": {
            "description": "Identity applied, but the device's push token could not be re-linked. The body is `{success: false, code: \"PUSH_LINKAGE_INCOMPLETE\", error, data}` where `data.results.identity.applied` is `true` and `data.pushRegistration` is `{applied: false, status: \"unconfirmed\"}`. Keep the new identity and retry identify.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartialFailure"
                }
              }
            }
          },
          "400": {
            "description": "No device id, an invalid device id, or a non-boolean `consent`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "deviceId must be a non-empty string, and not the reserved value \"anon\"",
                  "code": "INVALID_DEVICE_ID"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "The device id belongs to the other environment (test vs live key), code `DEVICE_ENVIRONMENT_MISMATCH`; or an erasure of this device is still incomplete, code `ERASURE_PENDING` (retry `DELETE /identify` with the same device id, and identify only once it answers `200`). Or, once the test environment is retired, a test key of a workspace with no sandbox cannot write, code `SANDBOX_REQUIRED`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "environmentMismatch": {
                    "summary": "Device or profile in the other environment",
                    "value": {
                      "success": false,
                      "error": "device belongs to a different environment",
                      "code": "DEVICE_ENVIRONMENT_MISMATCH"
                    }
                  },
                  "sandboxRequired": {
                    "summary": "Test key, workspace without a sandbox",
                    "value": {
                      "success": false,
                      "error": "This workspace has no sandbox: test keys can only read live content. Create a sandbox in Settings > Workspace",
                      "code": "SANDBOX_REQUIRED"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/GatewayUnavailable"
          },
          "503": {
            "description": "The identity cache could not be invalidated first. Retry. Also answered, with `code` `UPSTREAM_UNAVAILABLE` and `Retry-After`, when a created publishable key could not be validated; retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "Identity cache invalidation is unconfirmed; retry identify"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Identity & lifecycle"
        ],
        "operationId": "deleteIdentity",
        "summary": "Erase this device's data (GDPR / PDPL)",
        "description": "**Destructive.** Erases everything this device id addresses: the device record, its linked user profile, KYC, location history, geofence events, enrichment, analytics events and popup frequency data (counters and the durable once-ever / stop state for blocks and campaigns). Wipe the device id on the client only after a `200` (`localWipeSafe: true`). A `207` means some parts were erased and others were not: `data.remaining` lists them and `data.pendingSince` dates the request. Keep the device id and retry with it. The `207` is sent only to a client that declares `erasure-partial-v1` in `X-CF-SDK-Capabilities`; any other client gets the `502` for a partial erasure, because SDKs released before the capability may treat any 2xx as a completed erasure. A `502` means the erasure could not be confirmed at all, or was partial for a client without the capability; retry the same way. When nothing was erased, a client that declares `erasure-partial-v1` also gets the per-part result in the `502`'s `data` (`status: \"failed\"`, `parts`, `remaining`, `pendingSince`). A `400` from the erasure service (an unusable device id) is passed through, not turned into a retryable `502`. Until the erasure completes, `POST /identify` for this device answers `409 ERASURE_PENDING`; afterwards the person's profile is recreated only once consent is granted again. A part that was not erased carries a fixed `code` and `error`, never the store's raw error. Try it only with a throwaway device id on a TEST key.\n\n**SDK:** `deleteIdentity()`",
        "parameters": [
          {
            "$ref": "#/components/parameters/DeviceRequired"
          }
        ],
        "responses": {
          "200": {
            "description": "Every part erased. Safe to drop the device id locally.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Success envelope.",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "deviceId": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "const": "deleted"
                        },
                        "localWipeSafe": {
                          "type": "boolean",
                          "const": true
                        },
                        "parts": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "object",
                            "properties": {
                              "store": {
                                "type": "string",
                                "enum": [
                                  "devices",
                                  "profiles",
                                  "location",
                                  "events",
                                  "frequency"
                                ],
                                "description": "The kind of data this part covers, as a public label. Informational only; branch on applied and localWipeSafe, not on this value."
                              },
                              "applied": {
                                "type": "boolean"
                              },
                              "deleted": {
                                "type": [
                                  "integer",
                                  "null"
                                ]
                              },
                              "code": {
                                "type": "string",
                                "enum": [
                                  "ERASURE_STORE_TIMEOUT",
                                  "ERASURE_STORE_UNAVAILABLE",
                                  "ERASURE_STORE_NOT_CONFIRMED",
                                  "ERASURE_STORE_NOT_REPORTED",
                                  "ERASURE_DEVICE_RETAINED",
                                  "ERASURE_DEVICE_NOT_ERASED",
                                  "ERASURE_DEVICE_UNREADABLE"
                                ],
                                "description": "Present when `applied` is false: why this part was not erased."
                              },
                              "error": {
                                "type": "string",
                                "description": "Present when `applied` is false: the fixed message for `code`."
                              }
                            }
                          }
                        },
                        "remaining": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "deviceId": "3f6c1a2e-8a4b-4f0e-9c1d-2b7e5d9a1c44",
                    "status": "deleted",
                    "localWipeSafe": true,
                    "remaining": [],
                    "parts": {
                      "device": {
                        "store": "devices",
                        "applied": true,
                        "deleted": 1
                      },
                      "profile": {
                        "store": "profiles",
                        "applied": true,
                        "deleted": 1
                      },
                      "kyc": {
                        "store": "profiles",
                        "applied": true,
                        "deleted": 0
                      },
                      "locationHistory": {
                        "store": "location",
                        "applied": true,
                        "deleted": 37
                      },
                      "geofenceEvents": {
                        "store": "location",
                        "applied": true,
                        "deleted": 4
                      },
                      "enrichment": {
                        "store": "profiles",
                        "applied": true,
                        "deleted": 2
                      },
                      "events": {
                        "store": "events",
                        "applied": true,
                        "deleted": 212
                      },
                      "frequencyCounters": {
                        "store": "frequency",
                        "applied": true,
                        "deleted": 12
                      },
                      "frequencyState": {
                        "store": "frequency",
                        "applied": true,
                        "deleted": 1
                      },
                      "campaignFrequencyState": {
                        "store": "frequency",
                        "applied": true,
                        "deleted": 0
                      }
                    }
                  }
                }
              }
            }
          },
          "207": {
            "description": "Partial erasure: some parts were erased and some were not. `localWipeSafe` is always false. Keep the device id and retry; `remaining` names what is left, and the device record is always among it, because it is erased last. Sent only to a client that declares `erasure-partial-v1` in `X-CF-SDK-Capabilities`; any other client gets the `502` instead.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "error",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": false
                    },
                    "error": {
                      "type": "string"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "deviceId": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "const": "partial"
                        },
                        "localWipeSafe": {
                          "type": "boolean",
                          "const": false
                        },
                        "parts": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "object",
                            "properties": {
                              "store": {
                                "type": "string",
                                "enum": [
                                  "devices",
                                  "profiles",
                                  "location",
                                  "events",
                                  "frequency"
                                ],
                                "description": "The kind of data this part covers, as a public label. Informational only; branch on applied and localWipeSafe, not on this value."
                              },
                              "applied": {
                                "type": "boolean"
                              },
                              "deleted": {
                                "type": [
                                  "integer",
                                  "null"
                                ]
                              },
                              "code": {
                                "type": "string",
                                "enum": [
                                  "ERASURE_STORE_TIMEOUT",
                                  "ERASURE_STORE_UNAVAILABLE",
                                  "ERASURE_STORE_NOT_CONFIRMED",
                                  "ERASURE_STORE_NOT_REPORTED",
                                  "ERASURE_DEVICE_RETAINED",
                                  "ERASURE_DEVICE_NOT_ERASED",
                                  "ERASURE_DEVICE_UNREADABLE"
                                ],
                                "description": "Present when `applied` is false: why this part was not erased."
                              },
                              "error": {
                                "type": "string",
                                "description": "Present when `applied` is false: the fixed message for `code`."
                              }
                            }
                          }
                        },
                        "remaining": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "pendingSince": {
                          "type": "string",
                          "format": "date-time",
                          "description": "When the erasure was first requested; present while parts remain."
                        }
                      }
                    }
                  }
                },
                "example": {
                  "success": false,
                  "error": "2 of 7 parts were not erased (device, events); keep the device id and retry",
                  "data": {
                    "deviceId": "3f6c1a2e-8a4b-4f0e-9c1d-2b7e5d9a1c44",
                    "status": "partial",
                    "localWipeSafe": false,
                    "remaining": [
                      "device",
                      "events"
                    ],
                    "pendingSince": "2026-10-02T10:15:00.000Z",
                    "parts": {
                      "device": {
                        "store": "devices",
                        "applied": false,
                        "deleted": null,
                        "code": "ERASURE_DEVICE_RETAINED",
                        "error": "retained on purpose: the device record holds the link this request needs to reach the parts above, so it is erased last, once they are all gone. Retry to finish."
                      },
                      "profile": {
                        "store": "profiles",
                        "applied": true,
                        "deleted": 1
                      },
                      "kyc": {
                        "store": "profiles",
                        "applied": true,
                        "deleted": 0
                      },
                      "locationHistory": {
                        "store": "location",
                        "applied": true,
                        "deleted": 37
                      },
                      "geofenceEvents": {
                        "store": "location",
                        "applied": true,
                        "deleted": 4
                      },
                      "enrichment": {
                        "store": "profiles",
                        "applied": true,
                        "deleted": 2
                      },
                      "events": {
                        "store": "events",
                        "applied": false,
                        "deleted": null,
                        "code": "ERASURE_STORE_UNAVAILABLE",
                        "error": "the store could not be reached; retry"
                      },
                      "frequencyCounters": {
                        "store": "frequency",
                        "applied": true,
                        "deleted": 12
                      },
                      "frequencyState": {
                        "store": "frequency",
                        "applied": true,
                        "deleted": 1
                      },
                      "campaignFrequencyState": {
                        "store": "frequency",
                        "applied": true,
                        "deleted": 0
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No `X-CF-Device` header, or a device id the erasure service refuses (passed through as-is). Not retryable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "X-CF-Device header is required for identity deletion"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "$ref": "#/components/responses/SandboxRequired"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "The erasure could not be confirmed (nothing was erased, an upstream failure, or an unreadable response), or it was partial and the client did not declare `erasure-partial-v1`. Keep the device id and retry. When nothing was erased and the client declares `erasure-partial-v1`, `data` carries the per-part result (the same shape as the `207`'s, with `status: \"failed\"`).",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "object",
                          "additionalProperties": true,
                          "description": "Only for a client that declares `erasure-partial-v1`, when nothing was erased: `status` (`failed`), `localWipeSafe` (false), `parts`, `remaining`, `pendingSince`."
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "success": false,
                  "error": "Identity erasure incomplete; some data may remain",
                  "code": "UPSTREAM_UNAVAILABLE",
                  "retryable": true
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/KeyValidationUnavailable"
          }
        },
        "x-destructive": true
      }
    },
    "/locales": {
      "get": {
        "tags": [
          "Localization"
        ],
        "operationId": "getLocales",
        "summary": "List the workspace's locales",
        "description": "Every locale the workspace has configured, with translation coverage. Use it to build a language picker. An empty list is a valid answer.\n\n**SDK:** `getLocales()`",
        "parameters": [
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          }
        ],
        "responses": {
          "200": {
            "description": "Locales.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Success envelope.",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "locales": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Locale"
                          }
                        },
                        "sourceLocale": {
                          "type": "string"
                        },
                        "version": {
                          "type": "string"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "locales": [
                      {
                        "code": "en",
                        "name": "English",
                        "native": "English",
                        "rtl": false,
                        "isSource": true,
                        "coverage": 100
                      },
                      {
                        "code": "ar",
                        "name": "Arabic",
                        "native": "العربية",
                        "rtl": true,
                        "isSource": false,
                        "coverage": 92
                      }
                    ],
                    "sourceLocale": "en",
                    "version": "W/\"locales-1c9e\""
                  }
                }
              }
            },
            "headers": {
              "ETag": {
                "description": "Weak validator for this payload. Send it back as `If-None-Match` to get `304`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/GatewayUnavailable"
          },
          "503": {
            "$ref": "#/components/responses/KeyValidationUnavailable"
          }
        }
      }
    },
    "/location": {
      "post": {
        "tags": [
          "Location & geo"
        ],
        "operationId": "submitLocationBatch",
        "summary": "Upload device location fixes and geofence events",
        "description": "Uploads location fixes and geofence transitions collected by the app. Accepted only for an identified device with analytics consent. Note: this route's success body is not wrapped in `data`, and a refusal is a `202` with `success: false` and a `reason`.\n\n**Geofence events** (`geofenceEvents[]`) are the OS geofence transitions the device saw for fences from `GET /geofences`. The gateway forwards them to the server, which applies each one through the same state as server-side evaluation, so a stay fires each trigger (enter, exit, dwell) once whether the device or the server sees it first. Send `lat`, `lng` and `timestamp` with every event. An event stamped more than 5 minutes in the future or more than 24 hours ago is ignored, as is an event for a fence this device was not given. For a polygon fence, an `enter` or `dwell` whose position is outside the polygon itself (devices monitor its bounding circle) is recorded as unconfirmed and fires nothing. Report `exit` for every fence you monitor, even when its `triggers.exit` is false: the server needs it to close the stay. The answer counts what was queued, not what fired.\n\n**SDK:** `requestLocation()`, `startLocationTracking()` (batched)\n\n**When location is switched off for the workspace** (`config.features.location: false` on `/sync`), this route acknowledges the batch with `202` and stores nothing: `{\"success\": true, \"data\": {\"accepted\": 0, \"dropReasons\": {\"feature_disabled\": 3}}, \"locationsQueued\": 0, \"geofenceEvents\": 0}`. Do not retry the batch.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Device"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "deviceId": {
                    "type": "string"
                  },
                  "userId": {
                    "type": "string"
                  },
                  "sessionId": {
                    "type": "string"
                  },
                  "trackingMode": {
                    "type": "string",
                    "examples": [
                      "foreground",
                      "background",
                      "significant",
                      "continuous",
                      "one_shot"
                    ],
                    "description": "The SDK's tracking mode. `single`, `one_shot` (or `one-shot`, `oneshot`) store the batch as a single fix and `geofence` as geofence tracking; any other value, or none, is treated as continuous tracking."
                  },
                  "locations": {
                    "type": "array",
                    "maxItems": 100,
                    "items": {
                      "type": "object",
                      "properties": {
                        "lat": {
                          "type": "number"
                        },
                        "lng": {
                          "type": "number"
                        },
                        "accuracy": {
                          "type": "number"
                        },
                        "timestamp": {
                          "type": "string"
                        },
                        "source": {
                          "type": "string",
                          "enum": [
                            "gps",
                            "network",
                            "passive"
                          ]
                        }
                      }
                    },
                    "description": "At most 100 per request (`400 TOO_MANY_LOCATIONS` above that). The SDKs send 10 per batch by default."
                  },
                  "geofenceEvents": {
                    "type": "array",
                    "maxItems": 100,
                    "items": {
                      "type": "object",
                      "properties": {
                        "fenceId": {
                          "type": "string",
                          "description": "The `id` of a fence from `GET /geofences`."
                        },
                        "event": {
                          "type": "string",
                          "enum": [
                            "enter",
                            "exit",
                            "dwell"
                          ]
                        },
                        "dwellTime": {
                          "type": "integer",
                          "deprecated": true,
                          "description": "Ignored. Dwell time is decided on the server from the stay it recorded."
                        },
                        "lat": {
                          "type": "number",
                          "description": "Device latitude when the OS reported the transition. Required: a polygon `enter` or `dwell` is confirmed with it."
                        },
                        "lng": {
                          "type": "number",
                          "description": "Device longitude when the OS reported the transition."
                        },
                        "accuracy": {
                          "type": "number",
                          "description": "Horizontal accuracy of that position in metres, when the OS gives one."
                        },
                        "timestamp": {
                          "type": "string",
                          "description": "When the OS reported the transition (ISO 8601). More than 5 minutes in the future or more than 24 hours old is ignored."
                        },
                        "source": {
                          "type": "string",
                          "enum": [
                            "device"
                          ],
                          "description": "Always `device`. The server stamps it regardless of what is sent."
                        }
                      }
                    },
                    "description": "At most 100 per request (`400 TOO_MANY_GEOFENCE_EVENTS` above that; nothing is queued)."
                  }
                }
              },
              "example": {
                "deviceId": "3f6c1a2e-8a4b-4f0e-9c1d-2b7e5d9a1c44",
                "trackingMode": "foreground",
                "locations": [
                  {
                    "lat": 24.7136,
                    "lng": 46.6753,
                    "accuracy": 12,
                    "timestamp": "2026-09-29T10:15:00Z",
                    "source": "gps"
                  }
                ],
                "geofenceEvents": [
                  {
                    "fenceId": "store-12",
                    "event": "enter",
                    "lat": 24.7136,
                    "lng": 46.6753,
                    "accuracy": 15,
                    "timestamp": "2026-09-29T10:15:00Z",
                    "source": "device"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Queued, or refused with a code (`DEVICE_NOT_IDENTIFIED`: call identify first; `LOCATION_CONSENT_REQUIRED`: analytics consent not granted). A refused batch can't succeed until identify or consent changes, so don't retry it as is. When the workspace has switched location off, the answer is `success: true` with `data.accepted: 0` and `data.dropReasons.feature_disabled`; nothing is stored, so don't re-send it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "description": "Present on success. When the workspace has switched location off, the batch is acknowledged but not stored: `accepted` is 0 and `dropReasons.feature_disabled` counts the dropped items.",
                      "properties": {
                        "accepted": {
                          "type": "integer",
                          "description": "Locations plus geofence events queued."
                        },
                        "locationsQueued": {
                          "type": "integer"
                        },
                        "geofenceEvents": {
                          "type": "integer"
                        },
                        "dropReasons": {
                          "type": "object",
                          "description": "Only when items were dropped.",
                          "additionalProperties": {
                            "type": "integer"
                          },
                          "properties": {
                            "feature_disabled": {
                              "type": "integer"
                            }
                          }
                        }
                      }
                    },
                    "locationsQueued": {
                      "type": "integer",
                      "description": "Same as `data.locationsQueued`; kept for older clients."
                    },
                    "geofenceEvents": {
                      "type": "integer",
                      "description": "Same as `data.geofenceEvents`; kept for older clients."
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "DEVICE_NOT_IDENTIFIED",
                        "LOCATION_CONSENT_REQUIRED"
                      ]
                    },
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string",
                      "description": "Legacy free-text refusal reason; use `code`."
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "accepted": 2,
                    "locationsQueued": 1,
                    "geofenceEvents": 1
                  },
                  "locationsQueued": 1,
                  "geofenceEvents": 1
                }
              }
            }
          },
          "400": {
            "description": "No device id, invalid JSON, or a batch over the limits: more than 100 `geofenceEvents` (`TOO_MANY_GEOFENCE_EVENTS`) or more than 100 `locations` (`TOO_MANY_LOCATIONS`). Nothing was queued; split the batch and send again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "noDevice": {
                    "summary": "No device id",
                    "value": {
                      "success": false,
                      "error": "deviceId required"
                    }
                  },
                  "tooManyGeofenceEvents": {
                    "summary": "More than 100 geofence events",
                    "value": {
                      "success": false,
                      "error": "At most 100 geofenceEvents per request; split them into smaller batches. Nothing was queued",
                      "code": "TOO_MANY_GEOFENCE_EVENTS"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The body names a `userId` this device is not signed in as. Locations are always attributed to the device's own sign-in, so the batch is refused and nothing is stored. Omit `userId`, or call `identify()` with that user first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "device_not_identified": {
                    "summary": "Device not signed in as any user",
                    "value": {
                      "success": false,
                      "code": "DEVICE_NOT_IDENTIFIED",
                      "error": "this device is not signed in as a user; call identify() with this userId first"
                    }
                  },
                  "user_mismatch": {
                    "summary": "Device signed in as a different user",
                    "value": {
                      "success": false,
                      "code": "USER_MISMATCH",
                      "error": "this device is signed in as a different user"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/SandboxRequired"
          },
          "413": {
            "description": "The request body is larger than 1 MiB. Nothing was queued; send smaller batches.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "Request body is larger than 1048576 bytes; send smaller batches",
                  "code": "PAYLOAD_TOO_LARGE"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/KeyValidationUnavailable"
          }
        }
      }
    },
    "/logout": {
      "post": {
        "tags": [
          "Identity & lifecycle"
        ],
        "operationId": "logout",
        "summary": "Sign the device out",
        "description": "Unlinks the signed-in user from this device, clears its traits and segments, and detaches its push token from the user. Channel consent is kept. Call it when the user signs out.\n\n**SDK:** `logout()`",
        "parameters": [
          {
            "$ref": "#/components/parameters/Device"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "deviceId": {
                    "type": "string"
                  },
                  "userId": {
                    "type": "string",
                    "description": "Optional check: must match the device's linked user."
                  }
                }
              },
              "example": {
                "deviceId": "3f6c1a2e-8a4b-4f0e-9c1d-2b7e5d9a1c44"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signed out.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Success envelope.",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "additionalProperties": true
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "deviceId": "3f6c1a2e-8a4b-4f0e-9c1d-2b7e5d9a1c44",
                    "cleared": true,
                    "results": {
                      "identity": {
                        "applied": true,
                        "status": "anonymous"
                      },
                      "pushRegistration": {
                        "applied": true
                      }
                    },
                    "linkage": {
                      "status": "detached"
                    }
                  }
                }
              }
            }
          },
          "207": {
            "description": "Signed out, but push-token cleanup is unconfirmed. Retry logout.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartialFailure"
                },
                "example": {
                  "success": false,
                  "error": "device unlinked; push cleanup is unconfirmed, retry logout",
                  "code": "PUSH_LINKAGE_INCOMPLETE"
                }
              }
            }
          },
          "400": {
            "description": "No device id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "deviceId is required"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`userId` does not match the device.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "userId does not match this device",
                  "code": "USER_ID_DEVICE_MISMATCH"
                }
              }
            }
          },
          "409": {
            "description": "Device belongs to the other environment. Or, once the test environment is retired, a test key of a workspace with no sandbox cannot write, code `SANDBOX_REQUIRED`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "environmentMismatch": {
                    "summary": "Device or profile in the other environment",
                    "value": {
                      "success": false,
                      "error": "device belongs to a different environment",
                      "code": "DEVICE_ENVIRONMENT_MISMATCH"
                    }
                  },
                  "sandboxRequired": {
                    "summary": "Test key, workspace without a sandbox",
                    "value": {
                      "success": false,
                      "error": "This workspace has no sandbox: test keys can only read live content. Create a sandbox in Settings > Workspace",
                      "code": "SANDBOX_REQUIRED"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/GatewayUnavailable"
          },
          "503": {
            "$ref": "#/components/responses/KeyValidationUnavailable"
          }
        }
      }
    },
    "/profile": {
      "post": {
        "tags": [
          "Identity & lifecycle"
        ],
        "operationId": "updateProfile",
        "summary": "Update the signed-in user's domain profile",
        "description": "Writes industry/domain profile data (e.g. banking, travel) for the user this device is signed in as. The data is stored per category and is usable in segments. The device must have identified as this `userId` first; a user id in the body alone proves nothing.\n\n**SDK:** `updateIndustryProfile()`",
        "parameters": [
          {
            "$ref": "#/components/parameters/DeviceRequired"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "userId"
                ],
                "properties": {
                  "userId": {
                    "type": "string"
                  },
                  "category": {
                    "type": "string"
                  },
                  "data": {
                    "type": "object",
                    "additionalProperties": true
                  },
                  "domainProfile": {
                    "type": "object",
                    "description": "Several categories at once.",
                    "additionalProperties": {
                      "type": "object",
                      "additionalProperties": true
                    }
                  }
                }
              },
              "example": {
                "userId": "cust_1042",
                "category": "banking",
                "data": {
                  "segment": "premier",
                  "hasCreditCard": true
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated. The body is the profile service's response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "400": {
            "description": "Missing `userId`, missing `X-CF-Device`, or neither `category`+`data` nor `domainProfile`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "X-CF-Device header is required",
                  "code": "DEVICE_REQUIRED"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "This device is not signed in as `userId`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "this device is not signed in as a user; call identify() with this userId first",
                  "code": "DEVICE_NOT_IDENTIFIED"
                }
              }
            }
          },
          "409": {
            "description": "The profile belongs to the other environment: a test key cannot change a live customer's profile, code `DEVICE_ENVIRONMENT_MISMATCH`. Nothing is written. Or, once the test environment is retired, a test key of a workspace with no sandbox cannot write, code `SANDBOX_REQUIRED`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "environmentMismatch": {
                    "summary": "Device or profile in the other environment",
                    "value": {
                      "success": false,
                      "error": "profile belongs to a different environment",
                      "code": "DEVICE_ENVIRONMENT_MISMATCH"
                    }
                  },
                  "sandboxRequired": {
                    "summary": "Test key, workspace without a sandbox",
                    "value": {
                      "success": false,
                      "error": "This workspace has no sandbox: test keys can only read live content. Create a sandbox in Settings > Workspace",
                      "code": "SANDBOX_REQUIRED"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/GatewayUnavailable"
          },
          "503": {
            "$ref": "#/components/responses/KeyValidationUnavailable"
          }
        }
      }
    },
    "/push/preferences": {
      "patch": {
        "tags": [
          "Push"
        ],
        "operationId": "updatePushPreferences",
        "summary": "Update push preferences",
        "description": "Quiet hours, prayer-time respect, urgent-only and digest. Send them at the top level or under `preferences`.\n\n**SDK:** `updatePushPreferences()`",
        "parameters": [
          {
            "$ref": "#/components/parameters/Device"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/PushPreferences"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "deviceId": {
                        "type": "string"
                      },
                      "userId": {
                        "type": "string"
                      },
                      "preferences": {
                        "$ref": "#/components/schemas/PushPreferences"
                      }
                    }
                  }
                ]
              },
              "example": {
                "deviceId": "3f6c1a2e-8a4b-4f0e-9c1d-2b7e5d9a1c44",
                "quietHours": {
                  "enabled": true,
                  "start": "22:00",
                  "end": "07:00",
                  "timezone": "Asia/Riyadh"
                },
                "respectPrayerTimes": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stored.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Success envelope.",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "additionalProperties": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No recognised preference, no device, or no user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "preferences must include quietHours, respectPrayerTimes, urgentOnly or digest"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The body's `userId` is not the user this device is signed in as. Call `identify()` with that user first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "device_not_identified": {
                    "summary": "Device not signed in as any user",
                    "value": {
                      "success": false,
                      "code": "DEVICE_NOT_IDENTIFIED",
                      "error": "this device is not signed in as a user; call identify() with this userId first"
                    }
                  },
                  "user_mismatch": {
                    "summary": "Device signed in as a different user",
                    "value": {
                      "success": false,
                      "code": "USER_MISMATCH",
                      "error": "this device is signed in as a different user"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/SandboxRequired"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "The preference store failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "failed to update preferences"
                }
              }
            }
          },
          "502": {
            "$ref": "#/components/responses/GatewayUnavailable"
          },
          "503": {
            "$ref": "#/components/responses/KeyValidationUnavailable"
          }
        }
      }
    },
    "/push/topics": {
      "post": {
        "tags": [
          "Push"
        ],
        "operationId": "setPushTopics",
        "summary": "Subscribe or unsubscribe push topics",
        "description": "Either send `subscribe`/`unsubscribe` lists (incremental), or `topics` to replace the whole set. Needs a user: pass `userId` or identify first.\n\n**SDK:** `updatePushTopics()`",
        "parameters": [
          {
            "$ref": "#/components/parameters/Device"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "deviceId": {
                    "type": "string"
                  },
                  "userId": {
                    "type": "string"
                  },
                  "subscribe": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "unsubscribe": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "topics": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Replacement set."
                  }
                }
              },
              "example": {
                "deviceId": "3f6c1a2e-8a4b-4f0e-9c1d-2b7e5d9a1c44",
                "subscribe": [
                  "offers"
                ],
                "unsubscribe": [
                  "newsletter"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Success envelope.",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "deviceId": {
                          "type": "string"
                        },
                        "userId": {
                          "type": "string"
                        },
                        "topics": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "additionalProperties": true
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "deviceId": "3f6c1a2e-8a4b-4f0e-9c1d-2b7e5d9a1c44",
                    "topics": [
                      "offers",
                      "security_alerts"
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "No lists, an empty change, or the same topic in both lists.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "subscribe[] or unsubscribe[] required"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The body's `userId` is not the user this device is signed in as. Call `identify()` with that user first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "device_not_identified": {
                    "summary": "Device not signed in as any user",
                    "value": {
                      "success": false,
                      "code": "DEVICE_NOT_IDENTIFIED",
                      "error": "this device is not signed in as a user; call identify() with this userId first"
                    }
                  },
                  "user_mismatch": {
                    "summary": "Device signed in as a different user",
                    "value": {
                      "success": false,
                      "code": "USER_MISMATCH",
                      "error": "this device is signed in as a different user"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/SandboxRequired"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "The topic store failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "failed to set topics"
                }
              }
            }
          },
          "502": {
            "$ref": "#/components/responses/GatewayUnavailable"
          },
          "503": {
            "$ref": "#/components/responses/KeyValidationUnavailable"
          }
        }
      },
      "get": {
        "tags": [
          "Push"
        ],
        "operationId": "getPushTopics",
        "summary": "Get subscribed push topics",
        "description": "The topics the device's user is subscribed to. The device must be identified with a user.\n\n**SDK:** `getPushTopics()`",
        "parameters": [
          {
            "$ref": "#/components/parameters/DeviceRequired"
          }
        ],
        "responses": {
          "200": {
            "description": "Topics.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Success envelope.",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "deviceId": {
                          "type": "string"
                        },
                        "topics": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "deviceId": "3f6c1a2e-8a4b-4f0e-9c1d-2b7e5d9a1c44",
                    "topics": [
                      "offers"
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "No device id, or the device is not linked to a user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "device not linked to a user (identify first)"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/GatewayUnavailable"
          },
          "503": {
            "$ref": "#/components/responses/KeyValidationUnavailable"
          }
        }
      }
    },
    "/register-push": {
      "post": {
        "tags": [
          "Push"
        ],
        "operationId": "registerPush",
        "summary": "Register the device's push token",
        "description": "Links the OS push token (APNs, FCM, HMS or Web Push) to this installation and its user. Registering a token does **not** grant push consent; set channel consent separately. Call again whenever the OS refreshes the token.\n\n**SDK:** `registerPushWithReceipt()`, `registerPush()`",
        "parameters": [
          {
            "$ref": "#/components/parameters/Device"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "token"
                ],
                "properties": {
                  "deviceId": {
                    "type": "string"
                  },
                  "userId": {
                    "type": "string"
                  },
                  "token": {
                    "type": "string"
                  },
                  "platform": {
                    "type": "string",
                    "enum": [
                      "ios",
                      "apns",
                      "android",
                      "fcm",
                      "web",
                      "web_push",
                      "huawei",
                      "hms"
                    ]
                  },
                  "provider": {
                    "type": "string",
                    "enum": [
                      "apns",
                      "fcm",
                      "hms",
                      "web_push"
                    ],
                    "description": "Overrides the platform default, e.g. HMS on Android."
                  },
                  "hmsConfig": {
                    "type": "object",
                    "properties": {
                      "appId": {
                        "type": "string"
                      }
                    },
                    "description": "Required for HMS."
                  },
                  "topics": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "preferences": {
                    "$ref": "#/components/schemas/PushPreferences"
                  }
                }
              },
              "example": {
                "deviceId": "3f6c1a2e-8a4b-4f0e-9c1d-2b7e5d9a1c44",
                "token": "<device-token-from-the-OS>",
                "platform": "ios",
                "provider": "apns"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Registered.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Success envelope.",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "additionalProperties": true
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "deviceId": "3f6c1a2e-8a4b-4f0e-9c1d-2b7e5d9a1c44",
                    "registered": true,
                    "linkage": {
                      "status": "identified"
                    },
                    "provider": "apns",
                    "results": {
                      "device": {
                        "applied": true
                      },
                      "profile": {
                        "applied": true
                      }
                    }
                  }
                }
              }
            }
          },
          "207": {
            "description": "The device stored the token but the profile did not. Retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartialFailure"
                },
                "example": {
                  "success": false,
                  "error": "failed to store the push token on the profile"
                }
              }
            }
          },
          "400": {
            "description": "Missing token, invalid platform/provider, or HMS without `hmsConfig.appId`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "platform must be ios, apns, android, fcm, web, web_push, huawei, or hms",
                  "code": "INVALID_PUSH_PLATFORM"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`userId` differs from the device's identified user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "identify the intended user before registering push",
                  "code": "USER_ID_DEVICE_MISMATCH"
                }
              }
            }
          },
          "409": {
            "description": "Device belongs to the other environment. Or, once the test environment is retired, a test key of a workspace with no sandbox cannot write, code `SANDBOX_REQUIRED`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "environmentMismatch": {
                    "summary": "Device or profile in the other environment",
                    "value": {
                      "success": false,
                      "error": "device belongs to a different environment",
                      "code": "DEVICE_ENVIRONMENT_MISMATCH"
                    }
                  },
                  "sandboxRequired": {
                    "summary": "Test key, workspace without a sandbox",
                    "value": {
                      "success": false,
                      "error": "This workspace has no sandbox: test keys can only read live content. Create a sandbox in Settings > Workspace",
                      "code": "SANDBOX_REQUIRED"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/GatewayUnavailable"
          },
          "503": {
            "$ref": "#/components/responses/KeyValidationUnavailable"
          }
        }
      },
      "delete": {
        "tags": [
          "Push"
        ],
        "operationId": "unregisterPush",
        "summary": "Remove this device's push token",
        "description": "Removes only this installation's push token. Consent, traits, segments and topics are untouched. Idempotent: `removed: false` is a success.\n\n**SDK:** `unregisterPush()`",
        "parameters": [
          {
            "$ref": "#/components/parameters/Device"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "deviceId": {
                    "type": "string"
                  },
                  "userId": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "deviceId": "3f6c1a2e-8a4b-4f0e-9c1d-2b7e5d9a1c44"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Removed (or there was nothing to remove).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Success envelope.",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "deviceId": {
                          "type": "string"
                        },
                        "removed": {
                          "type": "boolean"
                        },
                        "results": {
                          "type": "object",
                          "additionalProperties": true
                        }
                      }
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "deviceId": "3f6c1a2e-8a4b-4f0e-9c1d-2b7e5d9a1c44",
                    "removed": true
                  }
                }
              }
            }
          },
          "400": {
            "description": "No device id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "deviceId is required",
                  "code": "DEVICE_ID_REQUIRED"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`userId` differs from the device's linked user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "...",
                  "code": "USER_ID_DEVICE_MISMATCH"
                }
              }
            }
          },
          "409": {
            "description": "Device belongs to the other environment. Or, once the test environment is retired, a test key of a workspace with no sandbox cannot write, code `SANDBOX_REQUIRED`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "environmentMismatch": {
                    "summary": "Device or profile in the other environment",
                    "value": {
                      "success": false,
                      "error": "...",
                      "code": "DEVICE_ENVIRONMENT_MISMATCH"
                    }
                  },
                  "sandboxRequired": {
                    "summary": "Test key, workspace without a sandbox",
                    "value": {
                      "success": false,
                      "error": "This workspace has no sandbox: test keys can only read live content. Create a sandbox in Settings > Workspace",
                      "code": "SANDBOX_REQUIRED"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/GatewayUnavailable"
          },
          "503": {
            "$ref": "#/components/responses/KeyValidationUnavailable"
          }
        }
      }
    },
    "/stream": {
      "get": {
        "tags": [
          "Live updates"
        ],
        "operationId": "stream",
        "summary": "Live change notifications (Server-Sent Events)",
        "description": "A long-lived `text/event-stream`. Each event names what changed (for example `block.published`, `block.updated`, `block.paused`) with an empty `data: {}`: treat it as a signal to call `/sync` again. Comment lines (`: connected`, `: ping`) keep the connection alive. On reconnect, send the last `id` as `Last-Event-ID` (or `?lastEventId=`) to replay what you missed. Browser `EventSource` cannot set headers, so this route also accepts the key as `?key=`. **Swagger UI cannot display a stream; use `curl -N` or `EventSource`.**\n\n**SDK:** `start()` (live updates), `subscribeLiveStatus()`\n\n**When live updates are switched off for the workspace** (`config.features.live_updates: false` on `/sync`), this route still answers `200 text/event-stream`, but sends only comment lines and a `retry:` hint, no events, holds the connection for about five minutes and then closes it. Keep polling `/sync` with `If-None-Match` in the meantime. It is never an error status, so no SDK reports a failure or stops for good.",
        "parameters": [
          {
            "name": "key",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Publishable key, for `EventSource` only."
          },
          {
            "name": "Last-Event-ID",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Device"
          }
        ],
        "responses": {
          "200": {
            "description": "Event stream.",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                },
                "example": ": connected\n\nid: 42\nevent: block.published\ndata: {}\n\n"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/GatewayUnavailable"
          },
          "503": {
            "$ref": "#/components/responses/KeyValidationUnavailable"
          }
        },
        "security": [
          {
            "publishableKey": []
          },
          {
            "streamKeyQuery": []
          }
        ]
      }
    },
    "/strings": {
      "get": {
        "tags": [
          "Localization"
        ],
        "operationId": "getStrings",
        "summary": "Get translated strings",
        "description": "Returns the workspace's strings for the resolved locale, optionally one namespace. When a translation is missing the workspace source text may be returned; the API never invents a translation. `smartKeys` lists keys marked as string sensors.\n\n**SDK:** `getStrings(namespace)`, `t(key)`",
        "parameters": [
          {
            "name": "namespace",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "example": "home"
          },
          {
            "$ref": "#/components/parameters/Device"
          },
          {
            "$ref": "#/components/parameters/Locale"
          },
          {
            "$ref": "#/components/parameters/AcceptLanguage"
          },
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          }
        ],
        "responses": {
          "200": {
            "description": "Strings.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Success envelope.",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "locale": {
                          "type": "string"
                        },
                        "strings": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "string"
                          }
                        },
                        "smartKeys": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "withheldKeys": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "version": {
                          "type": "string"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "locale": "ar",
                    "strings": {
                      "welcome": "أهلاً",
                      "cta.open": "افتح"
                    },
                    "smartKeys": [
                      "welcome"
                    ],
                    "withheldKeys": [],
                    "version": "W/\"strings-ar-4be1\""
                  }
                }
              }
            },
            "headers": {
              "ETag": {
                "description": "Weak validator for this payload. Send it back as `If-None-Match` to get `304`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/GatewayUnavailable"
          },
          "503": {
            "$ref": "#/components/responses/KeyValidationUnavailable"
          }
        }
      }
    },
    "/sync": {
      "get": {
        "tags": [
          "Content"
        ],
        "operationId": "sync",
        "summary": "Get all blocks for this device",
        "description": "Returns every block currently published for this device's segments, locale and schedule. This is the authoritative snapshot: a block absent from it is unpublished. Works anonymously with just the publishable key (no identify needed). Send `If-None-Match` to revalidate cheaply.\n\n**SDK:** `sync()`, `start()`\n\n**Workspace feature switches.** `data.config.features` lists the built-in SDK features a platform operator has switched on or off for this workspace (`true` = on). A missing key, or a missing `config`, means on. Changing a switch changes the `ETag`, so a revalidating client always gets the new config. Switches only ever turn a built-in feature on or off; they never change the base URL, keys, authentication, certificate pinning or consent. Switched-off features are also refused server side (see `/stream`, `/campaigns` and `/location`), so older SDKs that do not read `config` are covered too.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Device"
          },
          {
            "$ref": "#/components/parameters/Locale"
          },
          {
            "$ref": "#/components/parameters/AcceptLanguage"
          },
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          },
          {
            "$ref": "#/components/parameters/Capabilities"
          },
          {
            "$ref": "#/components/parameters/SdkVersion"
          },
          {
            "$ref": "#/components/parameters/SessionId"
          }
        ],
        "responses": {
          "200": {
            "description": "Current blocks.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Success envelope.",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "blocks": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Block"
                          }
                        },
                        "version": {
                          "type": "string"
                        },
                        "config": {
                          "type": "object",
                          "description": "Workspace SDK configuration. Optional: absent from older servers, and every field inside it is optional too.",
                          "properties": {
                            "features": {
                              "type": "object",
                              "description": "Per-workspace SDK feature switches, set by ContentFlow platform operators. `true` = on. A missing key means on.",
                              "properties": {
                                "live_updates": {
                                  "type": "boolean",
                                  "description": "Live updates over `/stream`. Off: the stream sends no events and SDKs keep polling `/sync`."
                                },
                                "campaigns": {
                                  "type": "boolean",
                                  "description": "Campaign evaluation (`/campaigns`). Off: an empty campaign list."
                                },
                                "location": {
                                  "type": "boolean",
                                  "description": "Location and geofence ingest (`/location`). Off: batches are acknowledged and dropped."
                                },
                                "frequency_caps": {
                                  "type": "boolean",
                                  "description": "Popup frequency control. Off: the server applies no frequency rule (capped blocks and campaigns are delivered again) and SDKs ignore their local frequency ledger."
                                },
                                "content_sync": {
                                  "type": "boolean",
                                  "description": "Content delivery (`/sync`, `/blocks/{key}`). Off: `304` when the request revalidates content the app holds (the app keeps it), otherwise an empty block list (`/blocks/{key}`: `404`), so the app shows its built-in defaults."
                                },
                                "strings": {
                                  "type": "boolean",
                                  "description": "Remote strings (`/strings`, `/locales`). Off: `304` when the request revalidates, otherwise an empty catalog or locale list, so the app uses its bundled strings."
                                },
                                "analytics_events": {
                                  "type": "boolean",
                                  "description": "Analytics events (`/events`, `/track-event`). Off: batches are acknowledged (`202`) and dropped with `dropReasons.feature_disabled`; nothing is recorded."
                                },
                                "push_preferences": {
                                  "type": "boolean",
                                  "description": "Push topics and preferences (`POST /push/topics`, `PATCH /push/preferences`). Off: acknowledged, nothing stored. Push token registration is never switched off."
                                },
                                "geofences": {
                                  "type": "boolean",
                                  "description": "Device-monitored geofences (`/geofences`). Off: an empty list, so the SDK stops monitoring fences."
                                },
                                "profile_updates": {
                                  "type": "boolean",
                                  "description": "Profile writes (`/identify` contact fields, traits, device and location; `/profile`; `/track-event` contact fields). Off: the device is still registered and erasure is still enforced, nothing is written to the profile store, `/profile` answers `200` with `updated: false`, and the device keeps its last known segments."
                                }
                              },
                              "additionalProperties": {
                                "type": "boolean"
                              }
                            },
                            "frequency": {
                              "type": "object",
                              "description": "Popup frequency control: the zone frequency windows are counted in, and the workspace limits on interruptive messages (blocks and campaigns whose rule has `interruptive`, unless `bypassWorkspaceCap`). `0` switches a limit off.",
                              "properties": {
                                "timezone": {
                                  "type": "string",
                                  "description": "IANA zone; \"once per day\" resets at local midnight here.",
                                  "examples": [
                                    "Asia/Riyadh"
                                  ]
                                },
                                "fatigue": {
                                  "type": "object",
                                  "properties": {
                                    "maxPerSession": {
                                      "type": "integer",
                                      "minimum": 0,
                                      "description": "Interruptive messages per app session."
                                    },
                                    "maxPerDay": {
                                      "type": "integer",
                                      "minimum": 0,
                                      "description": "Interruptive messages per local day (also enforced by the server)."
                                    },
                                    "minGapSeconds": {
                                      "type": "integer",
                                      "minimum": 0,
                                      "description": "Minimum time between any two interruptive messages."
                                    }
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "blocks": [
                      {
                        "key": "home_banner",
                        "name": "Home banner",
                        "screen": "home",
                        "instanceId": "inst_7c2f",
                        "segment": "vip",
                        "version": 4,
                        "values": {
                          "#title": "أهلاً بك من جديد",
                          "#image": "https://api.contentflow.click/sdk/v1/content/9b2d",
                          "#cta": "Open offer"
                        },
                        "fields": [
                          {
                            "tag": "#title",
                            "type": "text"
                          },
                          {
                            "tag": "#image",
                            "type": "image"
                          },
                          {
                            "tag": "#cta",
                            "type": "text"
                          }
                        ]
                      }
                    ],
                    "version": "W/\"sync-ws_1a2b-9f3c\"",
                    "config": {
                      "features": {
                        "live_updates": true,
                        "campaigns": true,
                        "location": true,
                        "frequency_caps": true
                      },
                      "frequency": {
                        "timezone": "Asia/Riyadh",
                        "fatigue": {
                          "maxPerSession": 1,
                          "maxPerDay": 3,
                          "minGapSeconds": 1800
                        }
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "ETag": {
                "description": "Weak validator for this payload. Send it back as `If-None-Match` to get `304`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/GatewayUnavailable"
          },
          "503": {
            "$ref": "#/components/responses/KeyValidationUnavailable"
          }
        }
      }
    },
    "/track-event": {
      "post": {
        "tags": [
          "Events"
        ],
        "operationId": "trackIdentityEvent",
        "summary": "Record a sign-up or sign-in",
        "description": "Records an identity event for the user this device is signed in as, and updates their name/email/phone on the profile. Requires analytics consent and a prior identify with the same user; otherwise it is acknowledged with `accepted: false` and a reason.\n\n**SDK:** `trackSignUp()`, `trackSignIn()`",
        "parameters": [
          {
            "$ref": "#/components/parameters/Device"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "username"
                ],
                "properties": {
                  "username": {
                    "type": "string",
                    "description": "Must equal the device's identified userId."
                  },
                  "eventType": {
                    "type": "string",
                    "examples": [
                      "sign_up",
                      "sign_in"
                    ]
                  },
                  "deviceId": {
                    "type": "string"
                  },
                  "fullName": {
                    "type": "string"
                  },
                  "email": {
                    "type": "string"
                  },
                  "phone": {
                    "type": "string"
                  },
                  "device": {
                    "type": "object",
                    "additionalProperties": true
                  },
                  "location": {
                    "type": "object",
                    "additionalProperties": true
                  },
                  "custom": {
                    "type": "object",
                    "additionalProperties": true
                  }
                }
              },
              "example": {
                "username": "cust_1042",
                "eventType": "sign_up",
                "deviceId": "3f6c1a2e-8a4b-4f0e-9c1d-2b7e5d9a1c44",
                "fullName": "Sara Al-Qahtani",
                "email": "sara@example.com"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Acknowledged. `accepted: false` includes a `reason`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Success envelope.",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "accepted": {
                          "type": "boolean"
                        },
                        "eventType": {
                          "type": "string"
                        },
                        "reason": {
                          "type": "string"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "accepted": true,
                    "eventType": "SIGN_UP"
                  }
                }
              }
            }
          },
          "400": {
            "description": "`username` is missing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "username is required: ...",
                  "code": "USERNAME_REQUIRED"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The device is linked to a different user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "username mismatch: device is linked to a different userId"
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/SandboxRequired"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "The event queue is unavailable. Also answered, with `code` `UPSTREAM_UNAVAILABLE` and `Retry-After`, when a created publishable key could not be validated; retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error": "Event queue unavailable; event was not queued"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "publishableKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-CF-Key",
        "description": "Your workspace's publishable key, safe to ship in an app. Create it under **Settings → API keys** (type In-app, Live or Test): `<workspaceId>_cfpk` followed by 40 hex characters, shown once. The workspace-derived key, `<workspaceId>_test` (test) or `<workspaceId>_app` (live), is still accepted unless the workspace (or the platform) switches it off; it is not secret, so prefer a created key. `X-API-Key` is accepted as the header name. Your test key is created for you: copy it from the portal **Developers** page (`<workspaceId>_cfpk…`). Every authenticated response carries `X-CF-Env: live` or `X-CF-Env: test`, the environment the key resolved to (readable from browsers, so a tool can confirm which environment it is talking to). Keys of the form `pk_test_…`/`pk_live_…` are secret server/CI keys: they authenticate the same routes but must never be shipped in an app."
      },
      "streamKeyQuery": {
        "type": "apiKey",
        "in": "query",
        "name": "key",
        "description": "`/stream` only, for `EventSource`, which cannot send headers."
      },
      "writeKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-CF-Write-Key",
        "description": "Server/CI credential for `/cards/*`. Never ship it in an app."
      }
    },
    "parameters": {
      "Device": {
        "name": "X-CF-Device",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string"
        },
        "example": "3f6c1a2e-8a4b-4f0e-9c1d-2b7e5d9a1c44",
        "description": "Stable per-install device id your app generates and keeps. Personalises content and keys every device-scoped write."
      },
      "DeviceRequired": {
        "name": "X-CF-Device",
        "in": "header",
        "required": true,
        "schema": {
          "type": "string"
        },
        "example": "3f6c1a2e-8a4b-4f0e-9c1d-2b7e5d9a1c44",
        "description": "The device this call acts on."
      },
      "AcceptLanguage": {
        "name": "Accept-Language",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string"
        },
        "example": "ar-SA",
        "description": "Used for the locale when `?locale=` is absent. Only the first tag is read."
      },
      "Locale": {
        "name": "locale",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string"
        },
        "example": "ar",
        "description": "Content locale (BCP 47). Wins over `Accept-Language`."
      },
      "IfNoneMatch": {
        "name": "If-None-Match",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string"
        },
        "description": "The `ETag` from your last response. An unchanged payload returns `304` with no body."
      },
      "Capabilities": {
        "name": "X-CF-SDK-Capabilities",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string"
        },
        "example": "sponsored-surface-v1",
        "description": "Comma-separated client capabilities. Every SDK sends it from 2.2.2; an empty value declares none. From 2.3.0 every SDK declares `erasure-partial-v1`, which gets a partial erasure answered with `207` instead of `502`. From 2.4.0 every SDK also declares `frequency-v1` (it enforces popup frequency rules locally); it is informational and the server applies frequency rules to every client. `sponsored-surface-v1` opts in to sponsored instances."
      },
      "SdkVersion": {
        "name": "X-CF-SDK-Version",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string"
        },
        "example": "js/2.4.1",
        "description": "The SDK that sent the request, as `<platform>/<semver>`; platform is `js`, `react-native`, `swift`, `kotlin` or `flutter`. Every SDK sends it from 2.2.2, for diagnostics. The bare `<semver>` form (JS 2.2.1 and earlier) is still accepted."
      },
      "SessionId": {
        "name": "X-CF-Session-Id",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string",
          "pattern": "^[A-Za-z0-9._-]{1,128}$"
        },
        "example": "sess_3f6c1a2e8a4b4f0e",
        "description": "The SDK's current session id: the same value it sends as `context.sessionId` on `POST /events` batches. From SDK 2.4.0 a session starts at a cold start and a new one starts after 30 minutes or more in the background and on logout or erasure; it is sent on `GET /sync`, `GET /blocks/{key}` and `POST /campaigns`. Optional. When present, popup frequency control also enforces session limits server-side (a rule's `maxPerSession` and the workspace popup limit per session), on top of the SDK's own ledger; without it those limits are enforced by the SDK only. Letters, digits, `.`, `_` and `-`, at most 128 characters; any other value is ignored."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing, malformed or revoked key, or a key naming a workspace that does not exist or is suspended. `code` is `MISSING_SDK_KEY` when no key was sent and `INVALID_SDK_KEY` when the key was refused, including a workspace-derived key (`<workspaceId>_app`/`_test`) after the workspace switched those off. `TENANT_KEY_MISMATCH`: a created publishable key was sent with an `X-Tenant-Id` naming another workspace. An unknown or suspended workspace gets the same answer as any other refused key, so a 401 does not reveal which workspaces exist.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "success": false,
              "error": "Missing API key",
              "code": "MISSING_SDK_KEY"
            }
          }
        }
      },
      "RateLimited": {
        "description": "A rate limit was exceeded. Retry after `Retry-After` seconds. The per-workspace limit answers `error` as an object (`code` `SDK_RATE_LIMIT_EXCEEDED`); the per-address limit (`IP_RATE_LIMITED`) and the per-user caps on consent changes and new push devices (`USER_RATE_LIMITED`) answer the standard error envelope.",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Limit": {
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "schema": {
              "type": "string"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "oneOf": [
                {
                  "$ref": "#/components/schemas/RateLimitError"
                },
                {
                  "$ref": "#/components/schemas/Error"
                }
              ]
            }
          }
        }
      },
      "GatewayUnavailable": {
        "description": "A downstream service could not be reached. Transient: `code` is `UPSTREAM_UNAVAILABLE`, `retryable` is `true`, and `Retry-After` gives the seconds to wait before retrying.",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "string"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "success": false,
              "error": "Service unavailable",
              "code": "UPSTREAM_UNAVAILABLE",
              "retryable": true
            }
          }
        }
      },
      "NotModified": {
        "description": "Unchanged since the `If-None-Match` you sent. No body.",
        "headers": {
          "ETag": {
            "description": "Weak validator for this payload. Send it back as `If-None-Match` to get `304`.",
            "schema": {
              "type": "string"
            }
          }
        }
      },
      "KeyValidationUnavailable": {
        "description": "A created publishable key could not be validated (the key service did not answer and the key has no recent successful validation). Transient: `code` is `UPSTREAM_UNAVAILABLE`, `retryable` is `true`, and `Retry-After` gives the seconds to wait.",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "string"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "success": false,
              "error": "SDK key validation is temporarily unavailable",
              "code": "UPSTREAM_UNAVAILABLE",
              "retryable": true
            }
          }
        }
      },
      "SandboxRequired": {
        "description": "Only when the platform has retired the in-workspace test environment (`TEST_ENV_RETIRED`): a test key of a workspace that has no sandbox can read live content but not write. Code `SANDBOX_REQUIRED`. Create a sandbox in Settings > Workspace. Nothing is written.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "success": false,
              "error": "This workspace has no sandbox: test keys can only read live content. Create a sandbox in Settings > Workspace",
              "code": "SANDBOX_REQUIRED"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "success",
          "error"
        ],
        "description": "Standard error envelope. `code` is present on errors that have a machine-readable cause. `retryable: true` marks a transient failure worth retrying after `Retry-After`.",
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "examples": [
              "MISSING_SDK_KEY",
              "INVALID_SDK_KEY",
              "UPSTREAM_UNAVAILABLE",
              "INVALID_DEVICE_ID",
              "DEVICE_ENVIRONMENT_MISMATCH",
              "USER_ID_DEVICE_MISMATCH",
              "ERASURE_PENDING",
              "TENANT_KEY_MISMATCH",
              "USER_RATE_LIMITED",
              "IP_RATE_LIMITED"
            ]
          },
          "retryable": {
            "type": "boolean",
            "const": true,
            "description": "Present only on transient failures (502, 503, 504)."
          }
        }
      },
      "PartialFailure": {
        "description": "A `207` answer: part of the request was applied and part was not. The standard error envelope plus `data`, whose `results` reports each part (`applied: true` or `false` with an `error`). Retry as the operation describes.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Error"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "object",
                "additionalProperties": true,
                "description": "What was stored. `results` reports each part."
              }
            }
          }
        ]
      },
      "RateLimitError": {
        "type": "object",
        "description": "Returned by the per-workspace rate limiter. Unlike the standard envelope, `error` is an object here.",
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "object",
            "properties": {
              "message": {
                "type": "string"
              },
              "code": {
                "type": "string",
                "const": "SDK_RATE_LIMIT_EXCEEDED"
              },
              "details": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        }
      },
      "EventInputError": {
        "type": "object",
        "description": "Batch rejected before anything was queued.",
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "code": {
            "type": "string",
            "enum": [
              "MAX_BATCH_SIZE",
              "INVALID_EVENT_TYPE",
              "ENDPOINT_MISMATCH",
              "INVALID_TIMESTAMP"
            ]
          },
          "error": {
            "type": "string"
          },
          "eventIndex": {
            "type": "integer",
            "description": "Index of the offending event."
          },
          "maxBatchSize": {
            "type": "integer",
            "const": 100
          },
          "data": {
            "type": "object",
            "properties": {
              "accepted": {
                "type": "integer",
                "const": 0
              }
            }
          }
        }
      },
      "FieldSchema": {
        "type": "object",
        "required": [
          "tag",
          "type"
        ],
        "properties": {
          "tag": {
            "type": "string",
            "examples": [
              "#title"
            ]
          },
          "type": {
            "type": "string",
            "examples": [
              "text",
              "image",
              "collection"
            ]
          },
          "allowedBlockKeys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Collection fields only."
          },
          "minItems": {
            "type": "integer"
          },
          "maxItems": {
            "type": "integer"
          }
        }
      },
      "Block": {
        "type": "object",
        "description": "A resolved block: one published instance chosen for this device's segments, locale and schedule.",
        "required": [
          "key",
          "name",
          "instanceId",
          "version",
          "values",
          "fields"
        ],
        "properties": {
          "key": {
            "type": "string",
            "description": "Block type key; the placement in your app.",
            "examples": [
              "home_banner"
            ]
          },
          "name": {
            "type": "string"
          },
          "screen": {
            "type": "string"
          },
          "instanceId": {
            "type": "string",
            "description": "Send this with impression/tap/conversion events."
          },
          "segment": {
            "type": [
              "string",
              "null"
            ],
            "description": "Segment the instance targets, or `null` when it targets everyone."
          },
          "version": {
            "type": "integer",
            "description": "Per-instance revision counter (not the ETag)."
          },
          "values": {
            "type": "object",
            "additionalProperties": true,
            "description": "Field values keyed by tag. A collection field holds an array of Block objects."
          },
          "fields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FieldSchema"
            }
          },
          "variantId": {
            "type": "string",
            "enum": [
              "A",
              "B"
            ],
            "description": "Present only when the instance runs an A/B test. Stamp it on events."
          },
          "sponsored": {
            "type": "object",
            "additionalProperties": true,
            "description": "Sponsored disclosure. Present only on sponsored instances, which are delivered only to callers that declare `sponsored-surface-v1` in `X-CF-SDK-Capabilities`."
          },
          "frequency": {
            "$ref": "#/components/schemas/FrequencyRule",
            "description": "Present only when the instance has an enabled frequency rule."
          },
          "priority": {
            "type": "number",
            "description": "Present only with `frequency`: the instance priority, higher first. `getPopup(screen)` ranks interruptive blocks and in-app campaigns together by it."
          },
          "source": {
            "type": "string",
            "enum": [
              "journey",
              "campaign"
            ],
            "description": "Present only on a message addressed to this user (instanceId `jm_<id>`, key `journey_message` by default): sent by a journey step or an in-app campaign. Dismissing it or tapping its CTA retires it."
          },
          "journeyId": {
            "type": "string",
            "description": "With `source: journey`: the journey that sent it."
          },
          "campaignId": {
            "type": "string",
            "description": "With `source: campaign`: the campaign that sent it. Its impressions, taps and conversions count toward that campaign."
          }
        }
      },
      "IdentifyRequest": {
        "type": "object",
        "properties": {
          "deviceId": {
            "type": "string",
            "description": "Stable per-install id. Falls back to `X-CF-Device`. Must not be empty or the reserved value `anon`."
          },
          "userId": {
            "type": "string",
            "description": "Your signed-in user id. Omit for anonymous installs."
          },
          "userIdType": {
            "type": "string",
            "enum": [
              "authenticated",
              "pseudonymous"
            ],
            "description": "How you minted `userId`. Absent means `authenticated`."
          },
          "consent": {
            "type": "boolean",
            "description": "Analytics consent. Omit to leave the stored decision unchanged; `true` grants, `false` revokes."
          },
          "traits": {
            "type": "object",
            "additionalProperties": true,
            "description": "Replaces the stored trait set when it has at least one key. Absent or `{}` leaves stored traits unchanged."
          },
          "platform": {
            "type": "string",
            "examples": [
              "ios",
              "android",
              "web"
            ]
          },
          "fullName": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "description": "Alias of `fullName`."
          },
          "email": {
            "type": "string"
          },
          "phone": {
            "type": "string"
          },
          "profileIfNew": {
            "type": "object",
            "description": "Gateway-only: profile fields written once, when the user's profile has no data yet (only after a confirmed, consented identify).",
            "properties": {
              "fullName": {
                "type": "string"
              },
              "email": {
                "type": "string"
              },
              "phone": {
                "type": "string"
              },
              "domainProfile": {
                "type": "object",
                "additionalProperties": {
                  "type": "object",
                  "additionalProperties": true
                }
              },
              "custom": {
                "type": "object",
                "additionalProperties": true
              }
            },
            "x-cf-gateway-only": true
          },
          "forceSync": {
            "type": "boolean",
            "description": "Gateway-only: write `profileIfNew` even if the profile already has data.",
            "x-cf-gateway-only": true
          }
        }
      },
      "OperationResult": {
        "type": "object",
        "properties": {
          "applied": {
            "type": "boolean"
          },
          "status": {
            "type": "string"
          },
          "error": {
            "type": "string"
          }
        }
      },
      "Locale": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "examples": [
              "ar"
            ]
          },
          "name": {
            "type": "string"
          },
          "native": {
            "type": "string"
          },
          "rtl": {
            "type": "boolean"
          },
          "isSource": {
            "type": "boolean"
          },
          "coverage": {
            "type": "number",
            "description": "Percent of strings translated (0-100)."
          }
        }
      },
      "PushPreferences": {
        "type": "object",
        "properties": {
          "quietHours": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "start": {
                "type": "string",
                "examples": [
                  "22:00"
                ]
              },
              "end": {
                "type": "string",
                "examples": [
                  "07:00"
                ]
              },
              "timezone": {
                "type": "string",
                "examples": [
                  "Asia/Riyadh"
                ]
              }
            }
          },
          "respectPrayerTimes": {
            "type": "boolean"
          },
          "urgentOnly": {
            "type": "boolean"
          },
          "digest": {
            "type": [
              "string",
              "null"
            ]
          },
          "frequency": {
            "type": "string",
            "description": "Legacy alias of `digest`."
          }
        }
      },
      "SdkEvent": {
        "type": "object",
        "required": [
          "type"
        ],
        "properties": {
          "type": {
            "type": "string",
            "description": "`impression`, `tap`, `click`, `conversion`, `cta_click`, `dismiss`, `string_impression`, `string_interaction`, a `category:action` name such as `commerce:purchase`, or your own custom name. `sign_up`/`sign_in` are rejected here; use `/track-event`.",
            "examples": [
              "impression",
              "commerce:purchase"
            ]
          },
          "category": {
            "type": "string"
          },
          "blockKey": {
            "type": "string"
          },
          "instanceId": {
            "type": "string"
          },
          "variantId": {
            "type": "string"
          },
          "experimentId": {
            "type": "string"
          },
          "campaignId": {
            "type": "string"
          },
          "blockType": {
            "type": "string"
          },
          "screen": {
            "type": "string"
          },
          "position": {
            "type": "integer"
          },
          "ts": {
            "description": "ISO-8601 timestamp or epoch milliseconds. Defaults to receive time.",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "integer"
              }
            ]
          },
          "custom": {
            "type": "object",
            "additionalProperties": true
          },
          "key": {
            "type": "string",
            "description": "String sensor events: the string key."
          },
          "stringKey": {
            "type": "string",
            "description": "Alias of `key`, read when `key` is absent or null."
          },
          "locale": {
            "type": "string"
          },
          "namespace": {
            "type": "string"
          },
          "path": {
            "type": "string"
          },
          "source": {
            "type": "string"
          }
        }
      },
      "BlockField": {
        "type": "object",
        "required": [
          "tag",
          "type",
          "label"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "tag": {
            "type": "string",
            "examples": [
              "#title"
            ]
          },
          "type": {
            "type": "string",
            "enum": [
              "text",
              "textarea",
              "richtext",
              "image",
              "video",
              "toggle",
              "select",
              "number",
              "date",
              "datetime",
              "color",
              "url",
              "json",
              "collection"
            ]
          },
          "label": {
            "type": "string"
          },
          "default": {},
          "options": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "maxLength": {
            "type": "integer"
          },
          "required": {
            "type": "boolean"
          },
          "allowedBlockKeys": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "BlockDefinition": {
        "type": "object",
        "required": [
          "key",
          "name",
          "fields"
        ],
        "properties": {
          "key": {
            "type": "string",
            "pattern": "^[a-z0-9_]+$",
            "examples": [
              "promo_card"
            ]
          },
          "name": {
            "type": "string"
          },
          "screen": {
            "type": "string"
          },
          "crumb": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "inCampaigns": {
            "type": "boolean"
          },
          "fields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BlockField"
            }
          }
        }
      },
      "FrequencyRule": {
        "type": "object",
        "description": "Popup frequency rule of a block instance or in-app campaign. The server already withholds an item that is capped for this user or device; SDKs keep a local ledger with the same rule for same-session, offline and cooldown enforcement (`shouldShow`, `getPopup`).",
        "required": [
          "enabled"
        ],
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "period": {
            "type": "string",
            "enum": [
              "hour",
              "day",
              "week",
              "month",
              "lifetime"
            ],
            "description": "Window for `maxImpressions` and `maxClicks`, in the workspace time zone (`config.frequency.timezone`)."
          },
          "maxImpressions": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000
          },
          "maxClicks": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000
          },
          "maxPerSession": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000,
            "description": "Enforced by the SDK, and by the server when the request carries `X-CF-Session-Id`."
          },
          "minIntervalSeconds": {
            "type": "integer",
            "minimum": 1,
            "maximum": 2592000,
            "description": "Cooldown between two shows."
          },
          "stopOn": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "cta_click",
                "dismiss",
                "conversion"
              ]
            },
            "description": "After any of these events the item is never shown to this user again."
          },
          "resetOnNewVersion": {
            "type": "boolean",
            "description": "Blocks: a republished instance (new `version`) counts as new content."
          },
          "interruptive": {
            "type": "boolean",
            "description": "Modal, popup or interstitial: subject to the workspace limits in `config.frequency.fatigue`."
          },
          "bypassWorkspaceCap": {
            "type": "boolean",
            "description": "Regulatory or transactional notice: skips the workspace limits but still obeys its own rule."
          }
        }
      }
    }
  }
}
