{
  "openapi": "3.1.1",
  "info": {
    "title": "Segmentic",
    "version": "v1",
    "summary": "The two customer-facing HTTP surfaces of Segmentic.",
    "description": "Two hosts, two kinds of key, and confusing them is the most expensive mistake an integration makes.\n\nThe ingest host takes events. Its key, `wk_seg_...`, is public by design: it ships inside your JavaScript bundle and inside your mobile app, it names an account and an app, and it can read nothing.\n\nThe management host reads and writes audiences, campaigns and reports. Its key, `sk_seg_...`, is a secret and belongs on a server. A write key offered here is refused with a distinct 401 rather than a generic one, so the mistake names itself.\n\nTwo things this document says that a vendor document usually does not. Error shapes are not uniform on the management host: eleven routes reuse handlers from the dashboard and answer with a flat `{\"error\": \"...\"}` string instead of the coded envelope, so parse `error` as a string or an object before reading `error.code`. And pagination does not work: `limit` is honoured on GET /v1/exports alone, `cursor` is honoured nowhere, `has_more` is always false, and the two list routes are cut at 200 rows by the query underneath with nothing in the response saying so.\n\nTwo behaviours of the management host are not modelled as operations, because they are not endpoints you call. Any unserved path answers 404 with `{\"error\":{\"code\":\"unknown_endpoint\",\"message\":\"no such endpoint: POST /v1/team/keys, see GET /v1/capabilities\"}}`, and a wrong method on a real path lands there too, so PUT /v1/campaigns/5 is a 404 rather than a 405.",
    "contact": {
      "name": "Segmentic",
      "url": "https://segmentic.net/docs"
    }
  },
  "servers": [
    {
      "url": "https://in.segmentic.net",
      "description": "Ingest. Takes events, device registrations and channel subscriptions. Authenticated by the public write key. Reachable from a browser: every response carries Access-Control-Allow-Origin: *, and credentials are never allowed."
    },
    {
      "url": "https://api.segmentic.net",
      "description": "Management. Reads and writes audiences, campaigns, reports and exports, and sends one transactional message at a time. Authenticated by a secret key that carries permissions. There is no CORS preflight handler on this mux, so a browser cannot call it."
    }
  ],
  "tags": [
    {
      "name": "ingest",
      "description": "https://in.segmentic.net, authenticated by the public write key `wk_seg_...`. This is the host your app and your website talk to."
    },
    {
      "name": "management",
      "description": "https://api.segmentic.net, authenticated by the secret key `sk_seg_...`. Server side only. It is not served at all until PUBLIC_API_ADDR is set, so check GET /v1/status before anything else."
    },
    {
      "name": "events",
      "description": "Sending events."
    },
    {
      "name": "channels",
      "description": "Devices, browser push, messengers and the in-app inbox."
    },
    {
      "name": "onsite",
      "description": "Banners, modals and surveys inside a page."
    },
    {
      "name": "messaging",
      "description": "What a message contains: pixels, links and one-off sends."
    },
    {
      "name": "webhooks",
      "description": "Inbound from other platforms and from mail transports."
    },
    {
      "name": "introspection",
      "description": "What this key is, and what this deployment serves."
    },
    {
      "name": "schema",
      "description": "Which events and traits this account holds."
    },
    {
      "name": "audiences",
      "description": "Ad-hoc filters, validated and counted."
    },
    {
      "name": "segments",
      "description": "Saved audiences."
    },
    {
      "name": "campaigns",
      "description": "Campaigns and their reports."
    },
    {
      "name": "exports",
      "description": "Asking for data to leave the building."
    },
    {
      "name": "reports",
      "description": "Funnels and retention."
    }
  ],
  "paths": {
    "/v1/status": {
      "get": {
        "operationId": "status",
        "tags": [
          "ingest",
          "management"
        ],
        "servers": [
          {
            "url": "https://in.segmentic.net",
            "description": "Ingest. Takes events, device registrations and channel subscriptions. Authenticated by the public write key. Reachable from a browser: every response carries Access-Control-Allow-Origin: *, and credentials are never allowed."
          },
          {
            "url": "https://api.segmentic.net",
            "description": "Management. Reads and writes audiences, campaigns, reports and exports, and sends one transactional message at a time. Authenticated by a secret key that carries permissions. There is no CORS preflight handler on this mux, so a browser cannot call it."
          }
        ],
        "security": [],
        "summary": "Is this surface up, and what is its clock",
        "description": "The one path that exists on both hosts, unauthenticated on both, and the only route on the management host that survives a Redis outage, because every other one spends request budget and the budget fails closed. `service` tells you which host answered: `collector` or `api`. X-Server-Time exists so a page can separate 'we are slow' from 'the path between you and us is slow'. Check this first on the management host: PUBLIC_API_ADDR is empty by default, and an install that has not set it serves nothing there at all.",
        "responses": {
          "200": {
            "description": "Always 200 when the process is running. The body is built at start-up.",
            "headers": {
              "X-Server-Time": {
                "schema": {
                  "type": "string",
                  "format": "date-time"
                },
                "description": "RFC 3339, UTC."
              },
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Always no-store, no-cache, must-revalidate."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatusResponse"
                },
                "examples": {
                  "ingest": {
                    "value": {
                      "status": "ok",
                      "service": "collector",
                      "version": "1.14.2"
                    }
                  },
                  "management": {
                    "value": {
                      "status": "ok",
                      "service": "api",
                      "version": "1.14.2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/track": {
      "post": {
        "operationId": "track",
        "tags": [
          "ingest",
          "events"
        ],
        "servers": [
          {
            "url": "https://in.segmentic.net",
            "description": "Ingest. Takes events, device registrations and channel subscriptions. Authenticated by the public write key. Reachable from a browser: every response carries Access-Control-Allow-Origin: *, and credentials are never allowed."
          }
        ],
        "security": [
          {
            "writeKey": []
          }
        ],
        "summary": "Record something a person did",
        "description": "The path decides the type. A `type` field in the body is ignored rather than honoured, so /v1/track can only ever produce a track event, and a caller who posts an attacker-controlled type gets a 200 and a track event. `event` is required here and nowhere else.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Envelope"
              },
              "examples": {
                "request": {
                  "value": {
                    "message_id": "m-1",
                    "event": "order_completed",
                    "user_id": "u_123",
                    "properties": {
                      "revenue": 2500000,
                      "city": "تهران"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted, or already accepted. A duplicate message_id answers exactly this, because an SDK that got an error would keep retrying for ever.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestResponse"
                },
                "examples": {
                  "accepted": {
                    "value": {
                      "status": "ok",
                      "accepted": 1
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body is not JSON, or the event failed validation. The message carries the stable sentinel and, for two of them, the value you sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "noIdentity": {
                    "value": {
                      "status": "error",
                      "message": "missing_identity"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No key was found in any of the three places, or the key is unknown, revoked, or belongs to a suspended account. Those three collapse into one answer so the endpoint cannot be used to find out which keys exist. Do not retry: nothing about the request will change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "invalid": {
                    "value": {
                      "status": "error",
                      "message": "invalid write key"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "The account is over a hard ceiling, cancelled, or out of trial. The message is Persian, always: the collector has no locale middleware, so Accept-Language has no effect here. Do not retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "eventCap": {
                    "value": {
                      "status": "error",
                      "message": "سقف رویدادهای این ماه پر شده است"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 5 MiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "tooLarge": {
                    "value": {
                      "status": "error",
                      "message": "request body too large"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The key could not be looked up, which is our failure and not yours. Carries Retry-After: 5. Keep the events and send them again. This status exists because the alternative was measured: with Postgres scaled to zero, eight of eight events came back 401, and an SDK reads 401 as permanent and discards the event.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 5."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "lookupDown": {
                    "value": {
                      "status": "error",
                      "message": "cannot verify the write key right now; retry"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/identify": {
      "post": {
        "operationId": "identify",
        "tags": [
          "ingest",
          "events"
        ],
        "servers": [
          {
            "url": "https://in.segmentic.net",
            "description": "Ingest. Takes events, device registrations and channel subscriptions. Authenticated by the public write key. Reachable from a browser: every response carries Access-Control-Allow-Origin: *, and credentials are never allowed."
          }
        ],
        "security": [
          {
            "writeKey": []
          }
        ],
        "summary": "Attach traits to a person",
        "description": "The event is always named `identify`, whatever `event` says. Four trait keys are treated specially: `email` is lower-cased, `phone` is parsed into E.164 and grows a `phone_operator` beside it, `national_id` is checked and dropped if the check digit fails, `gender` folds to male, female or other. Numeric traits are written twice, as text and as a number, which is what makes a filter like «balance over 100» work.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Envelope"
              },
              "examples": {
                "request": {
                  "value": {
                    "user_id": "gc_88123",
                    "traits": {
                      "gc_key_balance": 428,
                      "gc_is_active": true,
                      "gc_username": "hamid_gold",
                      "phone": "09123456789"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted, or already accepted. A duplicate message_id answers exactly this, because an SDK that got an error would keep retrying for ever.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestResponse"
                },
                "examples": {
                  "accepted": {
                    "value": {
                      "status": "ok",
                      "accepted": 1
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body is not JSON, or the event failed validation. The message carries the stable sentinel and, for two of them, the value you sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "noIdentity": {
                    "value": {
                      "status": "error",
                      "message": "missing_identity"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No key was found in any of the three places, or the key is unknown, revoked, or belongs to a suspended account. Those three collapse into one answer so the endpoint cannot be used to find out which keys exist. Do not retry: nothing about the request will change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "invalid": {
                    "value": {
                      "status": "error",
                      "message": "invalid write key"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "The account is over a hard ceiling, cancelled, or out of trial. The message is Persian, always: the collector has no locale middleware, so Accept-Language has no effect here. Do not retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "eventCap": {
                    "value": {
                      "status": "error",
                      "message": "سقف رویدادهای این ماه پر شده است"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 5 MiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "tooLarge": {
                    "value": {
                      "status": "error",
                      "message": "request body too large"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The key could not be looked up, which is our failure and not yours. Carries Retry-After: 5. Keep the events and send them again. This status exists because the alternative was measured: with Postgres scaled to zero, eight of eight events came back 401, and an SDK reads 401 as permanent and discards the event.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 5."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "lookupDown": {
                    "value": {
                      "status": "error",
                      "message": "cannot verify the write key right now; retry"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/page": {
      "post": {
        "operationId": "page",
        "tags": [
          "ingest",
          "events"
        ],
        "servers": [
          {
            "url": "https://in.segmentic.net",
            "description": "Ingest. Takes events, device registrations and channel subscriptions. Authenticated by the public write key. Reachable from a browser: every response carries Access-Control-Allow-Origin: *, and credentials are never allowed."
          }
        ],
        "security": [
          {
            "writeKey": []
          }
        ],
        "summary": "Record a web page view",
        "description": "`event` is optional. Without one the event is named `page_viewed`. Put the URL, path, title and referrer in `context.page`, not in properties: that is where reports read them from.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Envelope"
              },
              "examples": {
                "request": {
                  "value": {
                    "anonymous_id": "a-9f2c",
                    "event": "product",
                    "context": {
                      "page": {
                        "url": "https://shop.example.ir/p/1421",
                        "path": "/p/1421",
                        "title": "گوشی"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted, or already accepted. A duplicate message_id answers exactly this, because an SDK that got an error would keep retrying for ever.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestResponse"
                },
                "examples": {
                  "accepted": {
                    "value": {
                      "status": "ok",
                      "accepted": 1
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body is not JSON, or the event failed validation. The message carries the stable sentinel and, for two of them, the value you sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "noIdentity": {
                    "value": {
                      "status": "error",
                      "message": "missing_identity"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No key was found in any of the three places, or the key is unknown, revoked, or belongs to a suspended account. Those three collapse into one answer so the endpoint cannot be used to find out which keys exist. Do not retry: nothing about the request will change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "invalid": {
                    "value": {
                      "status": "error",
                      "message": "invalid write key"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "The account is over a hard ceiling, cancelled, or out of trial. The message is Persian, always: the collector has no locale middleware, so Accept-Language has no effect here. Do not retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "eventCap": {
                    "value": {
                      "status": "error",
                      "message": "سقف رویدادهای این ماه پر شده است"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 5 MiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "tooLarge": {
                    "value": {
                      "status": "error",
                      "message": "request body too large"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The key could not be looked up, which is our failure and not yours. Carries Retry-After: 5. Keep the events and send them again. This status exists because the alternative was measured: with Postgres scaled to zero, eight of eight events came back 401, and an SDK reads 401 as permanent and discards the event.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 5."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "lookupDown": {
                    "value": {
                      "status": "error",
                      "message": "cannot verify the write key right now; retry"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/screen": {
      "post": {
        "operationId": "screen",
        "tags": [
          "ingest",
          "events"
        ],
        "servers": [
          {
            "url": "https://in.segmentic.net",
            "description": "Ingest. Takes events, device registrations and channel subscriptions. Authenticated by the public write key. Reachable from a browser: every response carries Access-Control-Allow-Origin: *, and credentials are never allowed."
          }
        ],
        "security": [
          {
            "writeKey": []
          }
        ],
        "summary": "Record a mobile screen view",
        "description": "`event` is optional. Without one the event is named `screen_viewed`. The same shape as /v1/page, for an app rather than a site.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Envelope"
              },
              "examples": {
                "request": {
                  "value": {
                    "user_id": "u_123",
                    "event": "cart",
                    "context": {
                      "app": {
                        "version": "5.2.1"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted, or already accepted. A duplicate message_id answers exactly this, because an SDK that got an error would keep retrying for ever.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestResponse"
                },
                "examples": {
                  "accepted": {
                    "value": {
                      "status": "ok",
                      "accepted": 1
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body is not JSON, or the event failed validation. The message carries the stable sentinel and, for two of them, the value you sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "noIdentity": {
                    "value": {
                      "status": "error",
                      "message": "missing_identity"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No key was found in any of the three places, or the key is unknown, revoked, or belongs to a suspended account. Those three collapse into one answer so the endpoint cannot be used to find out which keys exist. Do not retry: nothing about the request will change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "invalid": {
                    "value": {
                      "status": "error",
                      "message": "invalid write key"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "The account is over a hard ceiling, cancelled, or out of trial. The message is Persian, always: the collector has no locale middleware, so Accept-Language has no effect here. Do not retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "eventCap": {
                    "value": {
                      "status": "error",
                      "message": "سقف رویدادهای این ماه پر شده است"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 5 MiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "tooLarge": {
                    "value": {
                      "status": "error",
                      "message": "request body too large"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The key could not be looked up, which is our failure and not yours. Carries Retry-After: 5. Keep the events and send them again. This status exists because the alternative was measured: with Postgres scaled to zero, eight of eight events came back 401, and an SDK reads 401 as permanent and discards the event.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 5."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "lookupDown": {
                    "value": {
                      "status": "error",
                      "message": "cannot verify the write key right now; retry"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/alias": {
      "post": {
        "operationId": "alias",
        "tags": [
          "ingest",
          "events"
        ],
        "servers": [
          {
            "url": "https://in.segmentic.net",
            "description": "Ingest. Takes events, device registrations and channel subscriptions. Authenticated by the public write key. Reachable from a browser: every response carries Access-Control-Allow-Origin: *, and credentials are never allowed."
          }
        ],
        "security": [
          {
            "writeKey": []
          }
        ],
        "summary": "Join an anonymous history to a signed-in account",
        "description": "`previous_id` is required, and it is the only endpoint that requires it. Send this immediately before the first identify after a sign-in, with `previous_id` set to the anonymous id the SDK has been using. Without it the person's whole pre-login history stays orphaned and every funnel that crosses the login boundary reports the wrong number.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Envelope"
              },
              "examples": {
                "request": {
                  "value": {
                    "user_id": "u_123",
                    "previous_id": "a-9f2c"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted, or already accepted. A duplicate message_id answers exactly this, because an SDK that got an error would keep retrying for ever.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestResponse"
                },
                "examples": {
                  "accepted": {
                    "value": {
                      "status": "ok",
                      "accepted": 1
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body is not JSON, or the event failed validation. The message carries the stable sentinel and, for two of them, the value you sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "noIdentity": {
                    "value": {
                      "status": "error",
                      "message": "missing_identity"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No key was found in any of the three places, or the key is unknown, revoked, or belongs to a suspended account. Those three collapse into one answer so the endpoint cannot be used to find out which keys exist. Do not retry: nothing about the request will change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "invalid": {
                    "value": {
                      "status": "error",
                      "message": "invalid write key"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "The account is over a hard ceiling, cancelled, or out of trial. The message is Persian, always: the collector has no locale middleware, so Accept-Language has no effect here. Do not retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "eventCap": {
                    "value": {
                      "status": "error",
                      "message": "سقف رویدادهای این ماه پر شده است"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 5 MiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "tooLarge": {
                    "value": {
                      "status": "error",
                      "message": "request body too large"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The key could not be looked up, which is our failure and not yours. Carries Retry-After: 5. Keep the events and send them again. This status exists because the alternative was measured: with Postgres scaled to zero, eight of eight events came back 401, and an SDK reads 401 as permanent and discards the event.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 5."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "lookupDown": {
                    "value": {
                      "status": "error",
                      "message": "cannot verify the write key right now; retry"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/batch": {
      "post": {
        "operationId": "batch",
        "tags": [
          "ingest",
          "events"
        ],
        "servers": [
          {
            "url": "https://in.segmentic.net",
            "description": "Ingest. Takes events, device registrations and channel subscriptions. Authenticated by the public write key. Reachable from a browser: every response carries Access-Control-Allow-Origin: *, and credentials are never allowed."
          }
        ],
        "security": [
          {
            "writeKey": []
          }
        ],
        "summary": "Send up to 500 events in one request",
        "description": "Each item carries its own `type`, and here it is load-bearing: an item with a missing or unknown type is rejected with `unknown_type`. Batch-level `context` and `sent_at` fill in only for items that have none of their own; a per-item value is never overwritten. A bad item does not fail the request. The response names the index of each rejection, so retry logic can find the item without matching on content. Duplicates count toward `accepted`, because the SDK already delivered them once and must stop retrying. The whole batch costs one de-duplication round trip and one publish, whether it holds two events or five hundred.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Batch"
              },
              "examples": {
                "request": {
                  "value": {
                    "context": {
                      "locale": "fa-IR",
                      "app": {
                        "version": "5.2.1"
                      }
                    },
                    "batch": [
                      {
                        "type": "track",
                        "message_id": "b1",
                        "event": "a",
                        "user_id": "u1"
                      },
                      {
                        "type": "track",
                        "message_id": "b2",
                        "event": "b",
                        "user_id": "u2"
                      },
                      {
                        "type": "identify",
                        "message_id": "b3",
                        "user_id": "u3",
                        "traits": {
                          "phone": "09123456789"
                        }
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Every item that could be accepted was. `accepted` is omitted when it is zero, so a wholly rejected batch answers with `rejected` and `errors` and no `accepted` key at all.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestResponse"
                },
                "examples": {
                  "allAccepted": {
                    "value": {
                      "status": "ok",
                      "accepted": 3
                    }
                  },
                  "partial": {
                    "value": {
                      "status": "ok",
                      "accepted": 2,
                      "rejected": 1,
                      "errors": [
                        {
                          "index": 1,
                          "reason": "missing_identity"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body is not JSON, the batch is empty, or it holds more than 500 items. All three refuse the whole request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "tooLarge": {
                    "value": {
                      "status": "error",
                      "message": "batch_too_large: 501 items, limit 500"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No key was found in any of the three places, or the key is unknown, revoked, or belongs to a suspended account. Those three collapse into one answer so the endpoint cannot be used to find out which keys exist. Do not retry: nothing about the request will change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "invalid": {
                    "value": {
                      "status": "error",
                      "message": "invalid write key"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "Over quota. The whole batch is refused, never a prefix: a partial accept would leave the SDK unable to tell which items to resend. Persian, and Accept-Language does not change it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "trialOver": {
                    "value": {
                      "status": "error",
                      "message": "دورهٔ آزمایشی به پایان رسیده است"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 5 MiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "tooLarge": {
                    "value": {
                      "status": "error",
                      "message": "request body too large"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The key could not be looked up, which is our failure and not yours. Carries Retry-After: 5. Keep the events and send them again. This status exists because the alternative was measured: with Postgres scaled to zero, eight of eight events came back 401, and an SDK reads 401 as permanent and discards the event.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 5."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "lookupDown": {
                    "value": {
                      "status": "error",
                      "message": "cannot verify the write key right now; retry"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/devices": {
      "post": {
        "operationId": "registerDevice",
        "tags": [
          "ingest",
          "channels"
        ],
        "servers": [
          {
            "url": "https://in.segmentic.net",
            "description": "Ingest. Takes events, device registrations and channel subscriptions. Authenticated by the public write key. Reachable from a browser: every response carries Access-Control-Allow-Origin: *, and credentials are never allowed."
          }
        ],
        "security": [
          {
            "writeKey": []
          }
        ],
        "summary": "Register a device so push can reach it",
        "description": "Registrations do not go through the event bus. They are a few per install per day and they have to be readable immediately: somebody who opens the app and enters a welcome journey two seconds later has to be reachable. A token on a transport its platform cannot use is kept out and warned about, never stored. Without that check the failure is not an error, it is a campaign that reports 100% sent and delivers nothing. Warnings come back on a 400 as well as a 200, because an SDK author sending an APNs token from an Android build sees only 'no usable token' otherwise and has nothing to go on.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeviceRegistration"
              },
              "examples": {
                "android": {
                  "value": {
                    "device_id": "d-3f19",
                    "user_id": "u_123",
                    "platform": "android",
                    "tokens": {
                      "fcm": "cOx9...",
                      "bazaar": "af92..."
                    },
                    "has_gms": true,
                    "push_enabled": true,
                    "app_version": "5.2.1",
                    "manufacturer": "Samsung",
                    "model": "SM-A515F"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stored. `warnings` lists anything that was altered or dropped.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeviceResponse"
                },
                "examples": {
                  "ok": {
                    "value": {
                      "status": "ok"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`device_id` is missing, the platform is unknown, neither identifier was given, or nothing usable was left after the transport check. The warnings ride along.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeviceResponse"
                },
                "examples": {
                  "wrongTransport": {
                    "value": {
                      "status": "error",
                      "message": "registration carries no usable token",
                      "warnings": [
                        {
                          "code": "transport_not_supported",
                          "message": "apns cannot deliver to android",
                          "field": "tokens"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No key was found in any of the three places, or the key is unknown, revoked, or belongs to a suspended account. Those three collapse into one answer so the endpoint cannot be used to find out which keys exist. Do not retry: nothing about the request will change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "invalid": {
                    "value": {
                      "status": "error",
                      "message": "invalid write key"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 5 MiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "tooLarge": {
                    "value": {
                      "status": "error",
                      "message": "request body too large"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The key could not be looked up, which is our failure and not yours. Carries Retry-After: 5. Keep the events and send them again. This status exists because the alternative was measured: with Postgres scaled to zero, eight of eight events came back 401, and an SDK reads 401 as permanent and discards the event.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 5."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "lookupDown": {
                    "value": {
                      "status": "error",
                      "message": "cannot verify the write key right now; retry"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/devices/unregister": {
      "post": {
        "operationId": "unregisterDevice",
        "tags": [
          "ingest",
          "channels"
        ],
        "servers": [
          {
            "url": "https://in.segmentic.net",
            "description": "Ingest. Takes events, device registrations and channel subscriptions. Authenticated by the public write key. Reachable from a browser: every response carries Access-Control-Allow-Origin: *, and credentials are never allowed."
          }
        ],
        "security": [
          {
            "writeKey": []
          }
        ],
        "summary": "Sign a person out of a device, or mark the install gone",
        "description": "Two different acts on one route. `revoked: false`, the default, is a sign-out: the user is detached and the token is kept, because the app is still installed. `revoked: true` is an uninstall. Call this on sign-out. On a shared phone, leaving the previous account attached means the next person receives someone else's order updates.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeviceUnregisterRequest"
              },
              "examples": {
                "signOut": {
                  "value": {
                    "device_id": "d-3f19",
                    "user_id": "u_123"
                  }
                },
                "uninstall": {
                  "value": {
                    "device_id": "d-3f19",
                    "revoked": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeviceResponse"
                },
                "examples": {
                  "ok": {
                    "value": {
                      "status": "ok"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`device_id` is required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeviceResponse"
                },
                "examples": {
                  "noId": {
                    "value": {
                      "status": "error",
                      "message": "device_id is required"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No key was found in any of the three places, or the key is unknown, revoked, or belongs to a suspended account. Those three collapse into one answer so the endpoint cannot be used to find out which keys exist. Do not retry: nothing about the request will change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "invalid": {
                    "value": {
                      "status": "error",
                      "message": "invalid write key"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 5 MiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "tooLarge": {
                    "value": {
                      "status": "error",
                      "message": "request body too large"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The key could not be looked up, which is our failure and not yours. Carries Retry-After: 5. Keep the events and send them again. This status exists because the alternative was measured: with Postgres scaled to zero, eight of eight events came back 401, and an SDK reads 401 as permanent and discards the event.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 5."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "lookupDown": {
                    "value": {
                      "status": "error",
                      "message": "cannot verify the write key right now; retry"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/webpush/subscribe": {
      "post": {
        "operationId": "webPushSubscribe",
        "tags": [
          "ingest",
          "channels"
        ],
        "servers": [
          {
            "url": "https://in.segmentic.net",
            "description": "Ingest. Takes events, device registrations and channel subscriptions. Authenticated by the public write key. Reachable from a browser: every response carries Access-Control-Allow-Origin: *, and credentials are never allowed."
          }
        ],
        "security": [
          {
            "writeKey": []
          }
        ],
        "summary": "Store a browser push subscription",
        "description": "Post what the browser handed you, without picking it apart and without re-encoding the keys. Base64 that has been decoded and re-encoded by a well-meaning helper is the classic way a subscription silently stops decrypting on the recipient's machine. The flattened form, with `endpoint`, `p256dh` and `auth` at the top level, is accepted too, for an SDK that already unpacked it. All four values are required: an endpoint with no keys is unusable, because the payload cannot be encrypted.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebPushSubscribeRequest"
              },
              "examples": {
                "nested": {
                  "value": {
                    "user_id": "u_9137",
                    "subscription": {
                      "endpoint": "https://fcm.googleapis.com/fcm/send/f9a1",
                      "p256dh": "BJ1cQ2h0",
                      "auth": "k2VrP0aQ"
                    }
                  }
                },
                "flattened": {
                  "value": {
                    "user_id": "u_9137",
                    "endpoint": "https://fcm.googleapis.com/fcm/send/f9a1",
                    "p256dh": "BJ1cQ2h0",
                    "auth": "k2VrP0aQ"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestResponse"
                },
                "examples": {
                  "ok": {
                    "value": {
                      "status": "ok"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A user id and a complete subscription are both required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "incomplete": {
                    "value": {
                      "status": "error",
                      "message": "user_id and a complete subscription are required"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No key was found in any of the three places, or the key is unknown, revoked, or belongs to a suspended account. Those three collapse into one answer so the endpoint cannot be used to find out which keys exist. Do not retry: nothing about the request will change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "invalid": {
                    "value": {
                      "status": "error",
                      "message": "invalid write key"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 5 MiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "tooLarge": {
                    "value": {
                      "status": "error",
                      "message": "request body too large"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The key could not be looked up, which is our failure and not yours. Carries Retry-After: 5. Keep the events and send them again. This status exists because the alternative was measured: with Postgres scaled to zero, eight of eight events came back 401, and an SDK reads 401 as permanent and discards the event.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 5."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "lookupDown": {
                    "value": {
                      "status": "error",
                      "message": "cannot verify the write key right now; retry"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/webpush/unsubscribe": {
      "post": {
        "operationId": "webPushUnsubscribe",
        "tags": [
          "ingest",
          "channels"
        ],
        "servers": [
          {
            "url": "https://in.segmentic.net",
            "description": "Ingest. Takes events, device registrations and channel subscriptions. Authenticated by the public write key. Reachable from a browser: every response carries Access-Control-Allow-Origin: *, and credentials are never allowed."
          }
        ],
        "security": [
          {
            "writeKey": []
          }
        ],
        "summary": "Forget a browser push subscription",
        "description": "Only `endpoint` is needed, and no user id is checked. The endpoint is the subscription's own secret: holding it is already enough to send to that browser, so demanding more before allowing somebody to stop receiving would be protecting the wrong direction.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebPushUnsubscribeRequest"
              },
              "examples": {
                "request": {
                  "value": {
                    "endpoint": "https://fcm.googleapis.com/fcm/send/f9a1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Removed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestResponse"
                },
                "examples": {
                  "ok": {
                    "value": {
                      "status": "ok"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`endpoint` is required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "noEndpoint": {
                    "value": {
                      "status": "error",
                      "message": "endpoint is required"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No key was found in any of the three places, or the key is unknown, revoked, or belongs to a suspended account. Those three collapse into one answer so the endpoint cannot be used to find out which keys exist. Do not retry: nothing about the request will change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "invalid": {
                    "value": {
                      "status": "error",
                      "message": "invalid write key"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 5 MiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "tooLarge": {
                    "value": {
                      "status": "error",
                      "message": "request body too large"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The key could not be looked up, which is our failure and not yours. Carries Retry-After: 5. Keep the events and send them again. This status exists because the alternative was measured: with Postgres scaled to zero, eight of eight events came back 401, and an SDK reads 401 as permanent and discards the event.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 5."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "lookupDown": {
                    "value": {
                      "status": "error",
                      "message": "cannot verify the write key right now; retry"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/messenger/link": {
      "post": {
        "operationId": "messengerLink",
        "tags": [
          "ingest",
          "channels"
        ],
        "servers": [
          {
            "url": "https://in.segmentic.net",
            "description": "Ingest. Takes events, device registrations and channel subscriptions. Authenticated by the public write key. Reachable from a browser: every response carries Access-Control-Allow-Origin: *, and credentials are never allowed."
          }
        ],
        "security": [
          {
            "writeKey": []
          }
        ],
        "summary": "Attach a Bale, Eitaa or Rubika chat to a person",
        "description": "`platform` accepts exactly `bale`, `eitaa` and `rubika`. Telegram is refused with a 400: the column carries a CHECK constraint, so an unrecognised value would fail in Postgres with an error nobody can act on. `source` records how you obtained the chat id. `bot_start` is the only route that carries real consent, and anything else is worth being able to find later. The collector passes it through verbatim and does not default it.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MessengerLinkRequest"
              },
              "examples": {
                "request": {
                  "value": {
                    "user_id": "u_9137",
                    "platform": "bale",
                    "chat_id": "44120099",
                    "source": "bot_start"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Linked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestResponse"
                },
                "examples": {
                  "ok": {
                    "value": {
                      "status": "ok"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A user id, a chat id and a known platform are all required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "bad": {
                    "value": {
                      "status": "error",
                      "message": "user_id, chat_id and a known platform are required"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No key was found in any of the three places, or the key is unknown, revoked, or belongs to a suspended account. Those three collapse into one answer so the endpoint cannot be used to find out which keys exist. Do not retry: nothing about the request will change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "invalid": {
                    "value": {
                      "status": "error",
                      "message": "invalid write key"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 5 MiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "tooLarge": {
                    "value": {
                      "status": "error",
                      "message": "request body too large"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The key could not be looked up, which is our failure and not yours. Carries Retry-After: 5. Keep the events and send them again. This status exists because the alternative was measured: with Postgres scaled to zero, eight of eight events came back 401, and an SDK reads 401 as permanent and discards the event.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 5."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "lookupDown": {
                    "value": {
                      "status": "error",
                      "message": "cannot verify the write key right now; retry"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/messenger/unlink": {
      "post": {
        "operationId": "messengerUnlink",
        "tags": [
          "ingest",
          "channels"
        ],
        "servers": [
          {
            "url": "https://in.segmentic.net",
            "description": "Ingest. Takes events, device registrations and channel subscriptions. Authenticated by the public write key. Reachable from a browser: every response carries Access-Control-Allow-Origin: *, and credentials are never allowed."
          }
        ],
        "security": [
          {
            "writeKey": []
          }
        ],
        "summary": "Detach a messenger chat from a person",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MessengerUnlinkRequest"
              },
              "examples": {
                "request": {
                  "value": {
                    "user_id": "u_9137",
                    "platform": "bale"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Unlinked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestResponse"
                },
                "examples": {
                  "ok": {
                    "value": {
                      "status": "ok"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A user id and a known platform are required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "bad": {
                    "value": {
                      "status": "error",
                      "message": "user_id and a known platform are required"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No key was found in any of the three places, or the key is unknown, revoked, or belongs to a suspended account. Those three collapse into one answer so the endpoint cannot be used to find out which keys exist. Do not retry: nothing about the request will change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "invalid": {
                    "value": {
                      "status": "error",
                      "message": "invalid write key"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 5 MiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "tooLarge": {
                    "value": {
                      "status": "error",
                      "message": "request body too large"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The key could not be looked up, which is our failure and not yours. Carries Retry-After: 5. Keep the events and send them again. This status exists because the alternative was measured: with Postgres scaled to zero, eight of eight events came back 401, and an SDK reads 401 as permanent and discards the event.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 5."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "lookupDown": {
                    "value": {
                      "status": "error",
                      "message": "cannot verify the write key right now; retry"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/inbox": {
      "post": {
        "operationId": "inbox",
        "tags": [
          "ingest",
          "channels"
        ],
        "servers": [
          {
            "url": "https://in.segmentic.net",
            "description": "Ingest. Takes events, device registrations and channel subscriptions. Authenticated by the public write key. Reachable from a browser: every response carries Access-Control-Allow-Origin: *, and credentials are never allowed."
          }
        ],
        "security": [
          {
            "writeKey": []
          }
        ],
        "summary": "Read one person's waiting in-app messages",
        "description": "The only ingest route with a second credential. `user_hash` is lower-case hex HMAC-SHA256(your tenant identity secret, user_id), computed by your own backend at sign-in and handed to the client. The write key says which account's data is in play; it is public and says nothing about who is asking. The hash says your backend authenticated this person. Only the second one stands between 'show me my messages' and 'show me everyone's'. There is no unverified mode. A POST rather than a GET for two reasons: the proof belongs in a body rather than a query string that every proxy, browser history and access log keeps a copy of, and reading the inbox has a side effect, the rows come back marked delivered.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InboxRequest"
              },
              "examples": {
                "request": {
                  "value": {
                    "user_id": "u_9137",
                    "user_hash": "3f1a9c02b7de4415aa0e8c1d2f6b37905c8e12ab34cd56ef78901234567890ab",
                    "limit": 20,
                    "seen": [
                      "c104.u_9137"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "`messages` is always an array, never null, so an SDK that iterates it without a nil check gets an empty loop rather than a crash.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InboxResponse"
                },
                "examples": {
                  "twoWaiting": {
                    "value": {
                      "status": "ok",
                      "messages": [
                        {
                          "message_id": "c104.u_9137",
                          "title": "سفارش شما ارسال شد",
                          "body": "کد رهگیری ۱۲۳۴",
                          "surface": "inbox",
                          "token": "1.7.k2.oe.mfk3z9.9f2a1c",
                          "created_at": "2026-08-06T09:12:41Z",
                          "expires_at": "2026-08-13T09:12:41Z",
                          "seen": false
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`user_id` is required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "noUser": {
                    "value": {
                      "status": "error",
                      "message": "user_id is required"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No key was found in any of the three places, or the key is unknown, revoked, or belongs to a suspended account. Those three collapse into one answer so the endpoint cannot be used to find out which keys exist. Do not retry: nothing about the request will change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "invalid": {
                    "value": {
                      "status": "error",
                      "message": "invalid write key"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The hash was wrong, missing, or your account has no identity secret configured. The same answer for all three: telling them apart would turn this into an oracle for which user ids exist.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "unverified": {
                    "value": {
                      "status": "error",
                      "message": "user identity is not verified"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 5 MiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "tooLarge": {
                    "value": {
                      "status": "error",
                      "message": "request body too large"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The key could not be looked up, which is our failure and not yours. Carries Retry-After: 5. Keep the events and send them again. This status exists because the alternative was measured: with Postgres scaled to zero, eight of eight events came back 401, and an SDK reads 401 as permanent and discards the event.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 5."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "lookupDown": {
                    "value": {
                      "status": "error",
                      "message": "cannot verify the write key right now; retry"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/inbox/ack": {
      "post": {
        "operationId": "inboxAck",
        "tags": [
          "ingest",
          "channels"
        ],
        "servers": [
          {
            "url": "https://in.segmentic.net",
            "description": "Ingest. Takes events, device registrations and channel subscriptions. Authenticated by the public write key. Reachable from a browser: every response carries Access-Control-Allow-Origin: *, and credentials are never allowed."
          }
        ],
        "security": [
          {
            "writeKey": []
          }
        ],
        "summary": "Mark inbox messages seen or dismissed",
        "description": "Same body and same identity proof as /v1/inbox. This does not record an engagement. Send `message_opened` through the ordinary event path with the `token` the inbox handed you, so that an in-app open is verified by the same signature check as every other channel's. A second, trusted path for the same signal would be trusted on the word of a caller holding nothing but a public write key.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InboxRequest"
              },
              "examples": {
                "request": {
                  "value": {
                    "user_id": "u_9137",
                    "user_hash": "3f1a9c02b7de4415aa0e8c1d2f6b37905c8e12ab34cd56ef78901234567890ab",
                    "seen": [
                      "c104.u_9137"
                    ],
                    "dismissed": [
                      "c99.u_9137"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InboxResponse"
                },
                "examples": {
                  "ok": {
                    "value": {
                      "status": "ok"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`user_id` is required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "noUser": {
                    "value": {
                      "status": "error",
                      "message": "user_id is required"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No key was found in any of the three places, or the key is unknown, revoked, or belongs to a suspended account. Those three collapse into one answer so the endpoint cannot be used to find out which keys exist. Do not retry: nothing about the request will change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "invalid": {
                    "value": {
                      "status": "error",
                      "message": "invalid write key"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The identity proof did not check out.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "unverified": {
                    "value": {
                      "status": "error",
                      "message": "user identity is not verified"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 5 MiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "tooLarge": {
                    "value": {
                      "status": "error",
                      "message": "request body too large"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The key could not be looked up, which is our failure and not yours. Carries Retry-After: 5. Keep the events and send them again. This status exists because the alternative was measured: with Postgres scaled to zero, eight of eight events came back 401, and an SDK reads 401 as permanent and discards the event.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 5."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "lookupDown": {
                    "value": {
                      "status": "error",
                      "message": "cannot verify the write key right now; retry"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/onsite": {
      "get": {
        "operationId": "onsiteCampaigns",
        "tags": [
          "ingest",
          "onsite"
        ],
        "servers": [
          {
            "url": "https://in.segmentic.net",
            "description": "Ingest. Takes events, device registrations and channel subscriptions. Authenticated by the public write key. Reachable from a browser: every response carries Access-Control-Allow-Origin: *, and credentials are never allowed."
          }
        ],
        "security": [
          {
            "writeKey": []
          }
        ],
        "summary": "Fetch the live on-site campaigns",
        "description": "The only ingest route that takes its key in the query string as well as in a header, and the only GET a customer's code calls. It carries no user identity at all, which is what makes it cacheable: one response serves every visitor, which is the difference between one request per visitor per minute and one per page view across a whole site. Cache-Control is public, max-age=60. The targeting rules come back in the response and are readable in anybody's network tab. That is deliberate, and the rule vocabulary holds nothing a customer would mind a competitor seeing. A store failure answers 200 with an empty list, never a 5xx: this runs inside your page load, and a failure of ours must degrade to 'no banner today' rather than a console error on your site.",
        "parameters": [
          {
            "name": "write_key",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "The write key, as an alternative to the Authorization header. A plain cross-origin GET with no custom header needs no preflight, which is why the SDK puts it here."
          }
        ],
        "responses": {
          "200": {
            "description": "The campaigns that are live right now.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Always public, max-age=60."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OnsiteFetchResponse"
                },
                "examples": {
                  "oneBanner": {
                    "value": {
                      "campaigns": [
                        {
                          "id": 12,
                          "name": "تخفیف نوروز",
                          "kind": "banner",
                          "status": "running",
                          "content": {
                            "headline": "۲۰ درصد تخفیف",
                            "button_text": "ببینید",
                            "button_url": "/sale",
                            "position": "bottom"
                          },
                          "targeting": {
                            "url_contains": [
                              "/p/"
                            ],
                            "delay_seconds": 5
                          },
                          "max_impressions": 3,
                          "cooldown_hours": 24,
                          "dismissible": true,
                          "impressions": 41200,
                          "clicks": 1804,
                          "dismissals": 622
                        }
                      ],
                      "cache_seconds": 60
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No key was found in any of the three places, or the key is unknown, revoked, or belongs to a suspended account. Those three collapse into one answer so the endpoint cannot be used to find out which keys exist. Do not retry: nothing about the request will change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "invalid": {
                    "value": {
                      "status": "error",
                      "message": "invalid write key"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 5 MiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "tooLarge": {
                    "value": {
                      "status": "error",
                      "message": "request body too large"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The key could not be looked up, which is our failure and not yours. Carries Retry-After: 5. Keep the events and send them again. This status exists because the alternative was measured: with Postgres scaled to zero, eight of eight events came back 401, and an SDK reads 401 as permanent and discards the event.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 5."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "lookupDown": {
                    "value": {
                      "status": "error",
                      "message": "cannot verify the write key right now; retry"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/onsite/event": {
      "post": {
        "operationId": "onsiteEvent",
        "tags": [
          "ingest",
          "onsite"
        ],
        "servers": [
          {
            "url": "https://in.segmentic.net",
            "description": "Ingest. Takes events, device registrations and channel subscriptions. Authenticated by the public write key. Reachable from a browser: every response carries Access-Control-Allow-Origin: *, and credentials are never allowed."
          }
        ],
        "security": [
          {
            "writeKey": []
          }
        ],
        "summary": "Record an impression, click, dismissal or conversion",
        "description": "`action` accepts `impression` (also the empty string), `click`, `dismiss` and `convert`, case-insensitively. Anything else is a 400. A storage failure still answers 200. Losing an impression count costs a number on a dashboard; returning an error to a script running inside your page costs you a console error on every page view.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OnsiteEventRequest"
              },
              "examples": {
                "impression": {
                  "value": {
                    "campaign_id": 12,
                    "anonymous_id": "a-9f2c",
                    "action": "impression",
                    "page_url": "https://shop.example.ir/p/1421"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Recorded, or not recorded and not worth telling you about.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestResponse"
                },
                "examples": {
                  "ok": {
                    "value": {
                      "status": "ok"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`campaign_id` and one of `user_id` or `anonymous_id` are required, and `action` must be one of the four.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "unknownAction": {
                    "value": {
                      "status": "error",
                      "message": "unknown action"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No key was found in any of the three places, or the key is unknown, revoked, or belongs to a suspended account. Those three collapse into one answer so the endpoint cannot be used to find out which keys exist. Do not retry: nothing about the request will change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "invalid": {
                    "value": {
                      "status": "error",
                      "message": "invalid write key"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 5 MiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "tooLarge": {
                    "value": {
                      "status": "error",
                      "message": "request body too large"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The key could not be looked up, which is our failure and not yours. Carries Retry-After: 5. Keep the events and send them again. This status exists because the alternative was measured: with Postgres scaled to zero, eight of eight events came back 401, and an SDK reads 401 as permanent and discards the event.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 5."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "lookupDown": {
                    "value": {
                      "status": "error",
                      "message": "cannot verify the write key right now; retry"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/onsite/response": {
      "post": {
        "operationId": "onsiteSurveyResponse",
        "tags": [
          "ingest",
          "onsite"
        ],
        "servers": [
          {
            "url": "https://in.segmentic.net",
            "description": "Ingest. Takes events, device registrations and channel subscriptions. Authenticated by the public write key. Reachable from a browser: every response carries Access-Control-Allow-Origin: *, and credentials are never allowed."
          }
        ],
        "security": [
          {
            "writeKey": []
          }
        ],
        "summary": "Submit a survey answer",
        "description": "The campaign is loaded and checked rather than trusted: whether this is an NPS survey decides whether the score means anything, and the browser is not the authority on that. A campaign that is not a survey is refused. When the campaign is NPS the score must be present and 0 to 10, and a body that omits it is refused rather than read as zero; otherwise the score is forced to -1, which means no score. Free-text answers over 2000 characters are truncated rather than refused, because somebody who wrote three paragraphs about their delivery has said something worth keeping. Answering also records a conversion, so the same person is not asked the same question next week.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OnsiteEventRequest"
              },
              "examples": {
                "nps": {
                  "value": {
                    "campaign_id": 18,
                    "user_id": "u_9137",
                    "score": 9,
                    "answers": {
                      "why": "ارسال سریع بود"
                    },
                    "page_url": "https://shop.example.ir/thanks"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestResponse"
                },
                "examples": {
                  "ok": {
                    "value": {
                      "status": "ok"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No ids, an unknown campaign, or a validation failure such as an NPS score outside 0 to 10.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "unknownCampaign": {
                    "value": {
                      "status": "error",
                      "message": "unknown campaign"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No key was found in any of the three places, or the key is unknown, revoked, or belongs to a suspended account. Those three collapse into one answer so the endpoint cannot be used to find out which keys exist. Do not retry: nothing about the request will change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "invalid": {
                    "value": {
                      "status": "error",
                      "message": "invalid write key"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 5 MiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "tooLarge": {
                    "value": {
                      "status": "error",
                      "message": "request body too large"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The key could not be looked up, which is our failure and not yours. Carries Retry-After: 5. Keep the events and send them again. This status exists because the alternative was measured: with Postgres scaled to zero, eight of eight events came back 401, and an SDK reads 401 as permanent and discards the event.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 5."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "lookupDown": {
                    "value": {
                      "status": "error",
                      "message": "cannot verify the write key right now; retry"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/hooks/{source}/{token}": {
      "post": {
        "operationId": "platformWebhook",
        "tags": [
          "ingest",
          "webhooks"
        ],
        "servers": [
          {
            "url": "https://in.segmentic.net",
            "description": "Ingest. Takes events, device registrations and channel subscriptions. Authenticated by the public write key. Reachable from a browser: every response carries Access-Control-Allow-Origin: *, and credentials are never allowed."
          }
        ],
        "security": [],
        "summary": "Receive a webhook from a shop or payment platform",
        "description": "You do not build this URL. Save a connection in the panel and it hands you the whole thing to paste into the other platform's admin. The token is 24 random bytes and it is not the credential: it lives in another platform's admin panel and in its request logs, so it will leak, and all it reveals is which account to send a correctly signed payload to. The signature is HMAC-SHA256 over the raw bytes, base64, compared in constant time. The raw bytes are kept deliberately: verifying against a re-encoded body is the single most common webhook bug there is, because decoding and re-encoding changes key order, whitespace and numeric formatting. A payload we cannot transform answers 200 with accepted: 0. These platforms retry a non-2xx for days and the payload will not change.",
        "parameters": [
          {
            "name": "source",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "digikala",
                "basalam",
                "torob",
                "zarinpal",
                "woocommerce",
                "shopify",
                "segment"
              ]
            },
            "description": "Resolution is by source and token together. A token alone would let a Shopify signature be replayed against a WooCommerce connection, whose transform reads entirely different fields and would produce a nonsense event with a genuine signature."
          },
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "32 URL-safe characters, issued when the connection is saved."
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The other platform's own payload, verbatim. At most 1 MiB is read, and a larger body is truncated rather than refused. A Shopify order with two hundred line items is about 200 KB.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted, or read and deliberately ignored. `accepted` counts the events produced, which is zero for a topic we do not map.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookResponse"
                },
                "examples": {
                  "order": {
                    "value": {
                      "status": "ok",
                      "accepted": 1
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body could not be read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "unreadable": {
                    "value": {
                      "status": "error",
                      "message": "unreadable body"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The signature did not verify, or the shared credential was wrong.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "signature": {
                    "value": {
                      "status": "error",
                      "message": "signature mismatch"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Unknown source, unknown token and a disabled connection all answer this, so the endpoint cannot be used to discover which tokens exist.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "unknown": {
                    "value": {
                      "status": "error",
                      "message": "unknown webhook"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The events could not be published. The platform will retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestError"
                },
                "examples": {
                  "down": {
                    "value": {
                      "status": "error",
                      "message": "temporarily unavailable, please retry"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/bounce/{local}": {
      "post": {
        "operationId": "bounceReport",
        "tags": [
          "ingest",
          "webhooks"
        ],
        "servers": [
          {
            "url": "https://in.segmentic.net",
            "description": "Ingest. Takes events, device registrations and channel subscriptions. Authenticated by the public write key. Reachable from a browser: every response carries Access-Control-Allow-Origin: *, and credentials are never allowed."
          }
        ],
        "security": [],
        "summary": "Hand in an email bounce report",
        "description": "For a mail transport, not for your application. The address is the credential: the local part is a keyed MAC over the account and the recipient, minted when the message went out, and the address the report claims to be about is checked against the same MAC. Both `b7k.abc.def` and `b7k.abc.def@bounce.segmentic.net` work. A refusal says nothing about why: telling a prober which half of the check failed is a free oracle for forging the other half. Failures are plain text, not JSON, because the caller is a mail transport.",
        "parameters": [
          {
            "name": "local",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The VERP return-path local part, with or without the domain."
          },
          {
            "name": "source",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "webhook",
                "manual"
              ]
            },
            "description": "Anything else, including absent, is recorded as `verp`. A caller must not be able to name an arbitrary source."
          }
        ],
        "requestBody": {
          "required": true,
          "description": "A raw RFC 5322 message. Not JSON, not form encoded. At most 1 MiB.",
          "content": {
            "message/rfc822": {
              "schema": {
                "type": "string"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What we did with it. 2xx even for a report we did nothing with, because the usual caller treats a non-2xx as 'retry this delivery' and will otherwise hand us the same bounce every five minutes for four days.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BounceOutcome"
                },
                "examples": {
                  "hardBounce": {
                    "value": {
                      "reports": 1,
                      "recorded": 1,
                      "suppressed": 1,
                      "opted_out": 0,
                      "rejected": 0
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Plain text: `missing return path`, `empty report`, or `report refused`.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "413": {
            "description": "Plain text: `report too large`.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/e/o": {
      "get": {
        "operationId": "openPixel",
        "tags": [
          "ingest",
          "messaging"
        ],
        "servers": [
          {
            "url": "https://in.segmentic.net",
            "description": "Ingest. Takes events, device registrations and channel subscriptions. Authenticated by the public write key. Reachable from a browser: every response carries Access-Control-Allow-Origin: *, and credentials are never allowed."
          }
        ],
        "security": [],
        "summary": "The email open pixel",
        "description": "Reached by a mail client, never by your code. The account comes from the signature rather than from a write key, which is stronger: a write key is public by design and a signature is not. It always returns the same 43-byte transparent GIF, with 200, even when the token is forged, the message is unknown, the database is down, or no parameters were given at all. A broken image in the middle of a marketing email is the most visible defect a recipient can see, and none of those failures is theirs. Email clicks have no endpoint here on purpose. A click redirect would add a hop that can fail, a domain that can be blocklisted, and would count every click a corporate mail scanner makes while checking the message for malware, which on a business list can be most of them. The click parameters are stamped onto your own link instead, and the SDK already on the landing page reports the arrival.",
        "parameters": [
          {
            "name": "sg_mid",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The message id."
          },
          {
            "name": "sg_t",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Six dot-separated parts: version, tenant, key id, purpose and channel, sent-at in base 36, and the HMAC."
          }
        ],
        "responses": {
          "200": {
            "description": "A 1x1 transparent GIF, with Cache-Control: no-store, no-cache, must-revalidate, private and Pragma: no-cache. Without those, Gmail's image proxy fetches once, serves its copy for ever, and every reopen after the first is invisible.",
            "content": {
              "image/gif": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          }
        }
      }
    },
    "/e/u": {
      "get": {
        "operationId": "unsubscribePage",
        "tags": [
          "ingest",
          "messaging"
        ],
        "servers": [
          {
            "url": "https://in.segmentic.net",
            "description": "Ingest. Takes events, device registrations and channel subscriptions. Authenticated by the public write key. Reachable from a browser: every response carries Access-Control-Allow-Origin: *, and credentials are never allowed."
          }
        ],
        "security": [],
        "summary": "Show the unsubscribe confirmation page",
        "description": "Unsubscribing with GET does not unsubscribe. This renders a self-contained Persian page whose only control posts back to the same URL. The reason is measured, not theoretical: security scanners at most Iranian banks and large retailers follow every link in every incoming message before the recipient ever sees it. A GET that opted people out would unsubscribe an entire company list the moment the campaign arrived, silently, and the customer's first sign of it would be a reach report that collapsed. The page has no stylesheet, no font and no image: it is opened from a mail client on a connection we know nothing about, and a page that needs a second request is a page that sometimes arrives unstyled.",
        "parameters": [
          {
            "name": "sg_mid",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sg_t",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The confirmation page. No opt-out has been recorded.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "unsubscribe",
        "tags": [
          "ingest",
          "messaging"
        ],
        "servers": [
          {
            "url": "https://in.segmentic.net",
            "description": "Ingest. Takes events, device registrations and channel subscriptions. Authenticated by the public write key. Reachable from a browser: every response carries Access-Control-Allow-Origin: *, and credentials are never allowed."
          }
        ],
        "security": [],
        "summary": "Opt somebody out, the RFC 8058 one-click path",
        "description": "The opt-out is global, not per channel, even though the token names the channel. What the person pressing it means is 'stop'. Reading it as 'stop emailing me, but the SMS and the Bale messages continue' is both legally weaker and, to the recipient, indistinguishable from ignoring them. Any failure still answers 200, with a different sentence: a mailbox provider that sees an error here may present the message as unsubscribe-broken.",
        "parameters": [
          {
            "name": "sg_mid",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sg_t",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "«اشتراک شما لغو شد.» when it worked, «درخواست لغو اشتراک ثبت شد.» when it did not.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/e/p": {
      "get": {
        "operationId": "preferenceCentre",
        "tags": [
          "ingest",
          "messaging"
        ],
        "servers": [
          {
            "url": "https://in.segmentic.net",
            "description": "Ingest. Takes events, device registrations and channel subscriptions. Authenticated by the public write key. Reachable from a browser: every response carries Access-Control-Allow-Origin: *, and credentials are never allowed."
          }
        ],
        "security": [],
        "summary": "Show the preference centre",
        "description": "No JSON API, no bundle, no framework, and the form is a plain POST with no JavaScript. The person opening this arrived from a link in an email: they have no account here, no session, and quite possibly a connection that will not fetch a second asset. A bad signature, an unknown message and a missing recipient all return the same 400 page. A page that distinguished them would let somebody probe which message ids exist. A load failure renders a fallback page with an unsubscribe button rather than an error page: the alternative for somebody who wanted to stop receiving mail and hit an error page is the spam button, which costs the sending domain far more than one lost subscriber.",
        "parameters": [
          {
            "name": "sg_mid",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sg_t",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The preference page.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "The fallback page, which still carries an unsubscribe button.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "This deployment does not serve a preference centre.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "preferenceCentreSave",
        "tags": [
          "ingest",
          "messaging"
        ],
        "servers": [
          {
            "url": "https://in.segmentic.net",
            "description": "Ingest. Takes events, device registrations and channel subscriptions. Authenticated by the public write key. Reachable from a browser: every response carries Access-Control-Allow-Origin: *, and credentials are never allowed."
          }
        ],
        "security": [],
        "summary": "Save a preference change",
        "description": "A form POST, not JSON. `action` is `save` (or absent), `unsubscribe_all`, `resubscribe`, `pause` or `resume`; anything else is refused. `pause_days` accompanies `pause` and must parse as an integer; the page offers 30, 60 and 90 and the server accepts up to 365. An oversized pause is refused, not clamped: silently clamping is how somebody believes they set a limit they did not. A topic checkbox is named `t_<topicID>_<channel>` and every box on the page has a hidden companion `p_t_<topicID>_<channel>`. An unticked checkbox is absent from the POST body entirely, so reading only what arrived would make 'I unticked everything' indistinguishable from 'I changed nothing'. After a save the state is re-read rather than echoed back. Showing the submitted form back would tell the recipient a change took effect when it did not, and this screen's only value is that it is believed.",
        "parameters": [
          {
            "name": "sg_mid",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sg_t",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "action": {
                    "type": "string",
                    "enum": [
                      "save",
                      "unsubscribe_all",
                      "resubscribe",
                      "pause",
                      "resume"
                    ]
                  },
                  "pause_days": {
                    "type": "string"
                  }
                },
                "additionalProperties": {
                  "type": "string"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The page again, rendered from freshly read state.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "The form could not be parsed, or the update was invalid.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/s/{code}": {
      "get": {
        "operationId": "shortLink",
        "tags": [
          "ingest",
          "messaging"
        ],
        "servers": [
          {
            "url": "https://in.segmentic.net",
            "description": "Ingest. Takes events, device registrations and channel subscriptions. Authenticated by the public write key. Reachable from a browser: every response carries Access-Control-Allow-Origin: *, and credentials are never allowed."
          }
        ],
        "security": [],
        "summary": "Follow a shortened link from an SMS",
        "description": "In production this lives on its own short domain rather than on the ingest host, and the reason is arithmetic: an SMS is billed per part, a part of Persian text is 70 characters, and a ten-character saving on every link is real money on a large send. On the ingest host itself the path answers 404 by design. There is no token and none is possible: it is reached by a phone with nothing but the characters in the message. The code is derived from a hash that includes the account rather than issued in sequence, so guessing one is guessing a hash, and the worst a guess can do is redirect the guesser to somebody's public landing page. 302, not 301. A permanent redirect is cached by the handset and every follow after the first would never reach us, which turns the click count into a count of first-time followers without anything saying so.",
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "302": {
            "description": "The destination, in Location.",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Empty, unknown or unresolvable, and a database outage too. Plain text, never an error page: an unknown code is almost always a mistyped one, and there is nothing useful to say to somebody who read seven characters off a screenshot.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/v1/whoami": {
      "get": {
        "operationId": "whoami",
        "tags": [
          "management",
          "introspection"
        ],
        "servers": [
          {
            "url": "https://api.segmentic.net",
            "description": "Management. Reads and writes audiences, campaigns, reports and exports, and sends one transactional message at a time. Authenticated by a secret key that carries permissions. There is no CORS preflight handler on this mux, so a browser cannot call it."
          }
        ],
        "security": [
          {
            "managementKey": []
          }
        ],
        "summary": "What this key is and what it may do",
        "description": "Needs no permission: any valid key works. Costs one budget unit. `permissions` is the effective set, the role's grants intersected with the key's scopes, sorted alphabetically. Call it at start-up and fail there rather than on the one call a month that needs the permission you lack. There is no email, no name, no account slug and no tenant list, on purpose: a key introspecting itself does not need to know which human created it, and publishing that makes every key a small identity disclosure. `scoped` is false on every key the product can create today. Scopes are readable from the database and nothing in the product writes them.",
        "responses": {
          "200": {
            "description": "The key's own identity and grants.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Whoami"
                },
                "examples": {
                  "analystKey": {
                    "value": {
                      "tenant_id": 7,
                      "api_key_id": 3,
                      "role": "analyst",
                      "permissions": [
                        "analytics.read",
                        "audit.read",
                        "campaign.read",
                        "data.export",
                        "event.read",
                        "journey.read",
                        "member.read",
                        "profile.read",
                        "segment.read",
                        "settings.read",
                        "template.read"
                      ],
                      "scoped": false
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential, an unresolvable one, an expired key, a `wk_` write key, or a dashboard session. Four distinct codes: `unauthenticated`, `key_expired`, `write_key_rejected`, `api_key_required`. Sessions are refused outright so this surface has no ambient credential at all, which means there is nothing to forge a request with.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "error": {
                        "code": "unauthenticated",
                        "message": "a valid API key is required"
                      }
                    }
                  },
                  "wrongFamily": {
                    "value": {
                      "error": {
                        "code": "write_key_rejected",
                        "message": "that is an SDK write key (wk_…); this API needs a management key (sk_seg_…)"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key's role, intersected with its scopes, does not carry the permission. `need` names it exactly. Do not retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "forbidden": {
                    "value": {
                      "error": {
                        "code": "forbidden",
                        "message": "this key does not carry the permission, see GET /v1/whoami for what it does carry",
                        "need": "campaign.read"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "The key has spent its weighted request budget for the calendar minute. 600 units by default, which is roughly two heavy reports a minute or six hundred cheap ones. The budget is per key, not per account, so a runaway agent cannot exhaust the budget your order pipeline depends on. There are no X-RateLimit headers on this path.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 60. Conservative: the fixed minute may roll sooner."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "exhausted": {
                    "value": {
                      "error": {
                        "code": "budget_exhausted",
                        "message": "this key has spent its request budget for the minute"
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The budget could not be verified. This fails closed, unlike the transactional rate limiter, because an unmetered agent in a loop is the more expensive failure and nobody's checkout breaks because a report waited. A Redis outage therefore takes this whole surface down, including whoami and capabilities. No Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "budgetDown": {
                    "value": {
                      "error": {
                        "code": "budget_unavailable",
                        "message": "could not verify the request budget"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/capabilities": {
      "get": {
        "operationId": "capabilities",
        "tags": [
          "management",
          "introspection"
        ],
        "servers": [
          {
            "url": "https://api.segmentic.net",
            "description": "Management. Reads and writes audiences, campaigns, reports and exports, and sends one transactional message at a time. Authenticated by a secret key that carries permissions. There is no CORS preflight handler on this mux, so a browser cannot call it."
          }
        ],
        "security": [
          {
            "managementKey": []
          }
        ],
        "summary": "What this deployment serves, and its numbers",
        "description": "Needs no permission. Two Segmentic installs genuinely differ: most capabilities register only when their subsystem is configured, so a client that assumed the full surface would be writing against a fiction. Read the limits from here rather than hardcoding them. Six of the ten feature flags gate a route on this surface: `segments`, `campaigns`, `analytics`, `transactional`, `ingest` and `async_exports`, plus `campaign_approval` which adds the submit route. `export`, `import` and `journeys` correspond to no route here and are informational. `ingest` and `import` are the same boolean and can never disagree. Two of the five limits describe the dashboard rather than this surface: `max_preview_rows` and `estimate_sample` have no public route that honours them. And `query_timeout_sec` is not the deadline on the two report routes, which use 45 seconds and do not publish it.",
        "responses": {
          "200": {
            "description": "The feature flags and the limits.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Capabilities"
                },
                "examples": {
                  "fullInstall": {
                    "value": {
                      "version": "v1",
                      "features": {
                        "segments": true,
                        "campaigns": true,
                        "analytics": true,
                        "transactional": true,
                        "export": false,
                        "import": true,
                        "journeys": true,
                        "ingest": true,
                        "async_exports": true,
                        "campaign_approval": true
                      },
                      "limits": {
                        "max_page_size": 100,
                        "max_preview_rows": 100,
                        "max_batch_size": 500,
                        "estimate_sample": 100,
                        "query_timeout_sec": 30
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential, an unresolvable one, an expired key, a `wk_` write key, or a dashboard session. Four distinct codes: `unauthenticated`, `key_expired`, `write_key_rejected`, `api_key_required`. Sessions are refused outright so this surface has no ambient credential at all, which means there is nothing to forge a request with.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "error": {
                        "code": "unauthenticated",
                        "message": "a valid API key is required"
                      }
                    }
                  },
                  "wrongFamily": {
                    "value": {
                      "error": {
                        "code": "write_key_rejected",
                        "message": "that is an SDK write key (wk_…); this API needs a management key (sk_seg_…)"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key's role, intersected with its scopes, does not carry the permission. `need` names it exactly. Do not retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "forbidden": {
                    "value": {
                      "error": {
                        "code": "forbidden",
                        "message": "this key does not carry the permission, see GET /v1/whoami for what it does carry",
                        "need": "campaign.read"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "The key has spent its weighted request budget for the calendar minute. 600 units by default, which is roughly two heavy reports a minute or six hundred cheap ones. The budget is per key, not per account, so a runaway agent cannot exhaust the budget your order pipeline depends on. There are no X-RateLimit headers on this path.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 60. Conservative: the fixed minute may roll sooner."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "exhausted": {
                    "value": {
                      "error": {
                        "code": "budget_exhausted",
                        "message": "this key has spent its request budget for the minute"
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The budget could not be verified. This fails closed, unlike the transactional rate limiter, because an unmetered agent in a loop is the more expensive failure and nobody's checkout breaks because a report waited. A Redis outage therefore takes this whole surface down, including whoami and capabilities. No Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "budgetDown": {
                    "value": {
                      "error": {
                        "code": "budget_unavailable",
                        "message": "could not verify the request budget"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/schema/events": {
      "get": {
        "operationId": "eventSchema",
        "tags": [
          "management",
          "schema"
        ],
        "servers": [
          {
            "url": "https://api.segmentic.net",
            "description": "Management. Reads and writes audiences, campaigns, reports and exports, and sends one transactional message at a time. Authenticated by a secret key that carries permissions. There is no CORS preflight handler on this mux, so a browser cannot call it."
          }
        ],
        "security": [
          {
            "managementKey": []
          }
        ],
        "summary": "Which events this account has actually sent",
        "description": "Needs `event.read`. Costs five budget units. No parameters. `last_seen` is the most useful column here: a large volume with a last-seen three weeks ago is a broken integration, and volume alone looks healthy for a month afterwards because the window is ninety days. The 503 on this route is the flat dashboard error shape, not the envelope the rest of this surface uses.",
        "responses": {
          "200": {
            "description": "Every event name, with its volume and its property keys.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventSchemaResponse"
                },
                "examples": {
                  "catalogue": {
                    "value": {
                      "events": [
                        {
                          "name": "order_completed",
                          "volume": 184203,
                          "prop_keys": [
                            "revenue",
                            "order_id",
                            "currency"
                          ],
                          "last_seen": "2026-08-06"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential, an unresolvable one, an expired key, a `wk_` write key, or a dashboard session. Four distinct codes: `unauthenticated`, `key_expired`, `write_key_rejected`, `api_key_required`. Sessions are refused outright so this surface has no ambient credential at all, which means there is nothing to forge a request with.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "error": {
                        "code": "unauthenticated",
                        "message": "a valid API key is required"
                      }
                    }
                  },
                  "wrongFamily": {
                    "value": {
                      "error": {
                        "code": "write_key_rejected",
                        "message": "that is an SDK write key (wk_…); this API needs a management key (sk_seg_…)"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key's role, intersected with its scopes, does not carry the permission. `need` names it exactly. Do not retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "forbidden": {
                    "value": {
                      "error": {
                        "code": "forbidden",
                        "message": "this key does not carry event.read, see GET /v1/whoami for what it does carry",
                        "need": "event.read"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "The key has spent its weighted request budget for the calendar minute. 600 units by default, which is roughly two heavy reports a minute or six hundred cheap ones. The budget is per key, not per account, so a runaway agent cannot exhaust the budget your order pipeline depends on. There are no X-RateLimit headers on this path.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 60. Conservative: the fixed minute may roll sooner."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "exhausted": {
                    "value": {
                      "error": {
                        "code": "budget_exhausted",
                        "message": "this key has spent its request budget for the minute"
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The budget could not be verified. This fails closed, unlike the transactional rate limiter, because an unmetered agent in a loop is the more expensive failure and nobody's checkout breaks because a report waited. A Redis outage therefore takes this whole surface down, including whoami and capabilities. No Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "budgetDown": {
                    "value": {
                      "error": {
                        "code": "budget_unavailable",
                        "message": "could not verify the request budget"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/schema/traits": {
      "get": {
        "operationId": "traitSchema",
        "tags": [
          "management",
          "schema"
        ],
        "servers": [
          {
            "url": "https://api.segmentic.net",
            "description": "Management. Reads and writes audiences, campaigns, reports and exports, and sends one transactional message at a time. Authenticated by a secret key that carries permissions. There is no CORS preflight handler on this mux, so a browser cannot call it."
          }
        ],
        "security": [
          {
            "managementKey": []
          }
        ],
        "summary": "Which profile traits this account holds",
        "description": "Needs `event.read`. Costs five budget units. Two keys carrying the same answer. `traits` stays a bare array of names because it is a public endpoint and something out there iterates it as strings; changing the element shape would break that integration on an upgrade with no error anywhere. `schema` is the richer answer beside it. `kind` decides which column a condition compiles against: a trait sent as both text and number appears once, as `number`, because the numeric column supports a range and the string one still answers equality.",
        "responses": {
          "200": {
            "description": "The names, and the same traits with their types and fill counts.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TraitSchemaResponse"
                },
                "examples": {
                  "traits": {
                    "value": {
                      "traits": [
                        "city",
                        "email",
                        "lifetime_value"
                      ],
                      "schema": [
                        {
                          "name": "city",
                          "kind": "string",
                          "users": 114943
                        },
                        {
                          "name": "email",
                          "kind": "string",
                          "users": 88120
                        },
                        {
                          "name": "lifetime_value",
                          "kind": "number",
                          "users": 61432
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential, an unresolvable one, an expired key, a `wk_` write key, or a dashboard session. Four distinct codes: `unauthenticated`, `key_expired`, `write_key_rejected`, `api_key_required`. Sessions are refused outright so this surface has no ambient credential at all, which means there is nothing to forge a request with.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "error": {
                        "code": "unauthenticated",
                        "message": "a valid API key is required"
                      }
                    }
                  },
                  "wrongFamily": {
                    "value": {
                      "error": {
                        "code": "write_key_rejected",
                        "message": "that is an SDK write key (wk_…); this API needs a management key (sk_seg_…)"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key's role, intersected with its scopes, does not carry the permission. `need` names it exactly. Do not retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "forbidden": {
                    "value": {
                      "error": {
                        "code": "forbidden",
                        "message": "this key does not carry event.read, see GET /v1/whoami for what it does carry",
                        "need": "event.read"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "The key has spent its weighted request budget for the calendar minute. 600 units by default, which is roughly two heavy reports a minute or six hundred cheap ones. The budget is per key, not per account, so a runaway agent cannot exhaust the budget your order pipeline depends on. There are no X-RateLimit headers on this path.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 60. Conservative: the fixed minute may roll sooner."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "exhausted": {
                    "value": {
                      "error": {
                        "code": "budget_exhausted",
                        "message": "this key has spent its request budget for the minute"
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The budget could not be verified. This fails closed, unlike the transactional rate limiter, because an unmetered agent in a loop is the more expensive failure and nobody's checkout breaks because a report waited. A Redis outage therefore takes this whole surface down, including whoami and capabilities. No Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "budgetDown": {
                    "value": {
                      "error": {
                        "code": "budget_unavailable",
                        "message": "could not verify the request budget"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/audiences/validate": {
      "post": {
        "operationId": "validateAudience",
        "tags": [
          "management",
          "audiences"
        ],
        "servers": [
          {
            "url": "https://api.segmentic.net",
            "description": "Management. Reads and writes audiences, campaigns, reports and exports, and sends one transactional message at a time. Authenticated by a secret key that carries permissions. There is no CORS preflight handler on this mux, so a browser cannot call it."
          }
        ],
        "security": [
          {
            "managementKey": []
          }
        ],
        "summary": "Check a filter, and read back what it means in Persian",
        "description": "Needs `segment.read`. Costs one budget unit. Body cap 1 MiB, not the 8 MiB the write routes allow. `description_fa` is the point of this endpoint. It is the artefact that catches a misread filter: a caller who sees «کاربرانی که شهرشان تهران است» when they meant Mashhad has found their bug before spending a query. An invalid filter is 422 here, where the dashboard's own validator answers 200 with valid:false. 200-with-a-flag is right for a live-typing form and wrong for an integration whose error handling branches on status. Compilation bounds: depth 8, 200 nodes, 1000 values in a list, key length 128.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AudienceRequest"
              },
              "examples": {
                "tehran": {
                  "value": {
                    "definition": {
                      "version": 1,
                      "root": {
                        "kind": "group",
                        "op": "and",
                        "children": [
                          {
                            "kind": "trait",
                            "trait": "city",
                            "operator": "eq",
                            "value": {
                              "type": "string",
                              "str": "تهران"
                            }
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The filter compiles.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidateResponse"
                },
                "examples": {
                  "valid": {
                    "value": {
                      "valid": true,
                      "description_fa": "کاربرانی که شهرشان تهران است"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body is not JSON. Flat shape on this route, not the envelope: it comes from the shared decoder underneath.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FlatError"
                },
                "examples": {
                  "malformed": {
                    "value": {
                      "error": "malformed JSON"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential, an unresolvable one, an expired key, a `wk_` write key, or a dashboard session. Four distinct codes: `unauthenticated`, `key_expired`, `write_key_rejected`, `api_key_required`. Sessions are refused outright so this surface has no ambient credential at all, which means there is nothing to forge a request with.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "error": {
                        "code": "unauthenticated",
                        "message": "a valid API key is required"
                      }
                    }
                  },
                  "wrongFamily": {
                    "value": {
                      "error": {
                        "code": "write_key_rejected",
                        "message": "that is an SDK write key (wk_…); this API needs a management key (sk_seg_…)"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key's role, intersected with its scopes, does not carry the permission. `need` names it exactly. Do not retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "forbidden": {
                    "value": {
                      "error": {
                        "code": "forbidden",
                        "message": "this key does not carry segment.read, see GET /v1/whoami for what it does carry",
                        "need": "segment.read"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "The filter does not compile. The message is the compiler's own text.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "badOperator": {
                    "value": {
                      "error": {
                        "code": "filter_invalid",
                        "message": "segment: unsupported operator: \"nonsense\""
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "The key has spent its weighted request budget for the calendar minute. 600 units by default, which is roughly two heavy reports a minute or six hundred cheap ones. The budget is per key, not per account, so a runaway agent cannot exhaust the budget your order pipeline depends on. There are no X-RateLimit headers on this path.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 60. Conservative: the fixed minute may roll sooner."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "exhausted": {
                    "value": {
                      "error": {
                        "code": "budget_exhausted",
                        "message": "this key has spent its request budget for the minute"
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The budget could not be verified. This fails closed, unlike the transactional rate limiter, because an unmetered agent in a loop is the more expensive failure and nobody's checkout breaks because a report waited. A Redis outage therefore takes this whole surface down, including whoami and capabilities. No Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "budgetDown": {
                    "value": {
                      "error": {
                        "code": "budget_unavailable",
                        "message": "could not verify the request budget"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/audiences/count": {
      "post": {
        "operationId": "countAudience",
        "tags": [
          "management",
          "audiences"
        ],
        "servers": [
          {
            "url": "https://api.segmentic.net",
            "description": "Management. Reads and writes audiences, campaigns, reports and exports, and sends one transactional message at a time. Authenticated by a secret key that carries permissions. There is no CORS preflight handler on this mux, so a browser cannot call it."
          }
        ],
        "security": [
          {
            "managementKey": []
          }
        ],
        "summary": "Count the people a filter matches",
        "description": "Needs `segment.read`. Costs 25 budget units, the heaviest class, because it is a scan whose cost grows with your history. Twenty-four of these exhaust a minute's budget. The count is exact, never sampled: `approximate` is always false on this route. Two inconsistencies worth knowing before you write the client. The Persian sentence is called `description` here and `description_fa` on /v1/audiences/validate and on the segment writes. And an uncompilable filter is a flat 400 here where validate answers 422 `filter_invalid` for the same filter.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AudienceRequest"
              },
              "examples": {
                "tehran": {
                  "value": {
                    "definition": {
                      "version": 1,
                      "root": {
                        "kind": "group",
                        "op": "and",
                        "children": [
                          {
                            "kind": "trait",
                            "trait": "city",
                            "operator": "eq",
                            "value": {
                              "type": "string",
                              "str": "تهران"
                            }
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The count, and how long it took.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CountResponse"
                },
                "examples": {
                  "counted": {
                    "value": {
                      "count": 61432,
                      "approximate": false,
                      "description": "کاربرانی که شهرشان تهران است",
                      "took_ms": 812
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, or a filter that does not compile. Flat shape.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FlatError"
                },
                "examples": {
                  "badFilter": {
                    "value": {
                      "error": "segment: unsupported operator: \"nonsense\""
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential, an unresolvable one, an expired key, a `wk_` write key, or a dashboard session. Four distinct codes: `unauthenticated`, `key_expired`, `write_key_rejected`, `api_key_required`. Sessions are refused outright so this surface has no ambient credential at all, which means there is nothing to forge a request with.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "error": {
                        "code": "unauthenticated",
                        "message": "a valid API key is required"
                      }
                    }
                  },
                  "wrongFamily": {
                    "value": {
                      "error": {
                        "code": "write_key_rejected",
                        "message": "that is an SDK write key (wk_…); this API needs a management key (sk_seg_…)"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key's role, intersected with its scopes, does not carry the permission. `need` names it exactly. Do not retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "forbidden": {
                    "value": {
                      "error": {
                        "code": "forbidden",
                        "message": "this key does not carry segment.read, see GET /v1/whoami for what it does carry",
                        "need": "segment.read"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "The key has spent its weighted request budget for the calendar minute. 600 units by default, which is roughly two heavy reports a minute or six hundred cheap ones. The budget is per key, not per account, so a runaway agent cannot exhaust the budget your order pipeline depends on. There are no X-RateLimit headers on this path.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 60. Conservative: the fixed minute may roll sooner."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "exhausted": {
                    "value": {
                      "error": {
                        "code": "budget_exhausted",
                        "message": "this key has spent its request budget for the minute"
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The budget could not be verified. This fails closed, unlike the transactional rate limiter, because an unmetered agent in a loop is the more expensive failure and nobody's checkout breaks because a report waited. A Redis outage therefore takes this whole surface down, including whoami and capabilities. No Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "budgetDown": {
                    "value": {
                      "error": {
                        "code": "budget_unavailable",
                        "message": "could not verify the request budget"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/segments": {
      "get": {
        "operationId": "listSegments",
        "tags": [
          "management",
          "segments"
        ],
        "servers": [
          {
            "url": "https://api.segmentic.net",
            "description": "Management. Reads and writes audiences, campaigns, reports and exports, and sends one transactional message at a time. Authenticated by a secret key that carries permissions. There is no CORS preflight handler on this mux, so a browser cannot call it."
          }
        ],
        "security": [
          {
            "managementKey": []
          }
        ],
        "summary": "List every saved audience",
        "description": "Needs `segment.read`. Costs one budget unit. Not paginated, and cut short without saying so. `limit` and `cursor` are accepted by the URL and ignored by the handler, there is no `next_cursor`, and the query underneath ends in LIMIT 200 ordered by `updated_at` descending. An account with three hundred segments receives the two hundred most recently edited and nothing in the response says the rest exist. `last_size` is the audience the last time it was counted, shown with its timestamp rather than recomputed on load: a list page that ran two hundred warehouse counts would take ten seconds to open.",
        "responses": {
          "200": {
            "description": "Every segment. The array is `[]`, never null.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SegmentListResponse"
                },
                "examples": {
                  "one": {
                    "value": {
                      "segments": [
                        {
                          "id": 12,
                          "name": "تهرانی‌ها",
                          "kind": "dynamic",
                          "definition": {
                            "version": 1,
                            "root": {
                              "kind": "group",
                              "op": "and",
                              "children": [
                                {
                                  "kind": "trait",
                                  "trait": "city",
                                  "operator": "eq",
                                  "value": {
                                    "type": "string",
                                    "str": "تهران"
                                  }
                                }
                              ]
                            }
                          },
                          "description_fa": "کاربرانی که شهرشان تهران است",
                          "last_size": 61432,
                          "last_computed_at": "2026-08-06T09:00:00Z",
                          "updated_at": "2026-08-06T09:00:00Z"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential, an unresolvable one, an expired key, a `wk_` write key, or a dashboard session. Four distinct codes: `unauthenticated`, `key_expired`, `write_key_rejected`, `api_key_required`. Sessions are refused outright so this surface has no ambient credential at all, which means there is nothing to forge a request with.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "error": {
                        "code": "unauthenticated",
                        "message": "a valid API key is required"
                      }
                    }
                  },
                  "wrongFamily": {
                    "value": {
                      "error": {
                        "code": "write_key_rejected",
                        "message": "that is an SDK write key (wk_…); this API needs a management key (sk_seg_…)"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key's role, intersected with its scopes, does not carry the permission. `need` names it exactly. Do not retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "forbidden": {
                    "value": {
                      "error": {
                        "code": "forbidden",
                        "message": "this key does not carry segment.read, see GET /v1/whoami for what it does carry",
                        "need": "segment.read"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "The key has spent its weighted request budget for the calendar minute. 600 units by default, which is roughly two heavy reports a minute or six hundred cheap ones. The budget is per key, not per account, so a runaway agent cannot exhaust the budget your order pipeline depends on. There are no X-RateLimit headers on this path.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 60. Conservative: the fixed minute may roll sooner."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "exhausted": {
                    "value": {
                      "error": {
                        "code": "budget_exhausted",
                        "message": "this key has spent its request budget for the minute"
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The budget could not be verified. This fails closed, unlike the transactional rate limiter, because an unmetered agent in a loop is the more expensive failure and nobody's checkout breaks because a report waited. A Redis outage therefore takes this whole surface down, including whoami and capabilities. No Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "budgetDown": {
                    "value": {
                      "error": {
                        "code": "budget_unavailable",
                        "message": "could not verify the request budget"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createSegment",
        "tags": [
          "management",
          "segments"
        ],
        "servers": [
          {
            "url": "https://api.segmentic.net",
            "description": "Management. Reads and writes audiences, campaigns, reports and exports, and sends one transactional message at a time. Authenticated by a secret key that carries permissions. There is no CORS preflight handler on this mux, so a browser cannot call it."
          }
        ],
        "security": [
          {
            "managementKey": []
          }
        ],
        "summary": "Save a new audience",
        "description": "Needs `segment.write`. Costs one budget unit. Body cap 8 MiB. There is no idempotency here. A retried timeout creates a second segment. A `tenant_id` in the body has no effect at all, which is a property of not reading the field rather than of checking it. Unknown fields are accepted and ignored, so a typo in a field name is invisible; POST /v1/messages is the only route on this surface that refuses them. There is no `kind` field. Every segment created through this API is dynamic. Creating a static or realtime list is not possible here.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SegmentWriteRequest"
              },
              "examples": {
                "tehran": {
                  "value": {
                    "name": "تهرانی‌ها",
                    "definition": {
                      "version": 1,
                      "root": {
                        "kind": "group",
                        "op": "and",
                        "children": [
                          {
                            "kind": "trait",
                            "trait": "city",
                            "operator": "eq",
                            "value": {
                              "type": "string",
                              "str": "تهران"
                            }
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Saved. Read `description_fa` back before you point a campaign at it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SegmentWriteResponse"
                },
                "examples": {
                  "created": {
                    "value": {
                      "id": 12,
                      "name": "تهرانی‌ها",
                      "description_fa": "کاربرانی که شهرشان تهران است"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, or a blank name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "noName": {
                    "value": {
                      "error": {
                        "code": "name_required",
                        "message": "a segment needs a name"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential, an unresolvable one, an expired key, a `wk_` write key, or a dashboard session. Four distinct codes: `unauthenticated`, `key_expired`, `write_key_rejected`, `api_key_required`. Sessions are refused outright so this surface has no ambient credential at all, which means there is nothing to forge a request with.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "error": {
                        "code": "unauthenticated",
                        "message": "a valid API key is required"
                      }
                    }
                  },
                  "wrongFamily": {
                    "value": {
                      "error": {
                        "code": "write_key_rejected",
                        "message": "that is an SDK write key (wk_…); this API needs a management key (sk_seg_…)"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key's role, intersected with its scopes, does not carry the permission. `need` names it exactly. Do not retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "forbidden": {
                    "value": {
                      "error": {
                        "code": "forbidden",
                        "message": "this key does not carry segment.write, see GET /v1/whoami for what it does carry",
                        "need": "segment.write"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "The filter does not compile.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "badFilter": {
                    "value": {
                      "error": {
                        "code": "filter_invalid",
                        "message": "segment: unsupported operator: \"nonsense\""
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "The key has spent its weighted request budget for the calendar minute. 600 units by default, which is roughly two heavy reports a minute or six hundred cheap ones. The budget is per key, not per account, so a runaway agent cannot exhaust the budget your order pipeline depends on. There are no X-RateLimit headers on this path.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 60. Conservative: the fixed minute may roll sooner."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "exhausted": {
                    "value": {
                      "error": {
                        "code": "budget_exhausted",
                        "message": "this key has spent its request budget for the minute"
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The budget could not be verified. This fails closed, unlike the transactional rate limiter, because an unmetered agent in a loop is the more expensive failure and nobody's checkout breaks because a report waited. A Redis outage therefore takes this whole surface down, including whoami and capabilities. No Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "budgetDown": {
                    "value": {
                      "error": {
                        "code": "budget_unavailable",
                        "message": "could not verify the request budget"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/segments/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "format": "int64",
            "minimum": 1
          }
        }
      ],
      "get": {
        "operationId": "getSegment",
        "tags": [
          "management",
          "segments"
        ],
        "servers": [
          {
            "url": "https://api.segmentic.net",
            "description": "Management. Reads and writes audiences, campaigns, reports and exports, and sends one transactional message at a time. Authenticated by a secret key that carries permissions. There is no CORS preflight handler on this mux, so a browser cannot call it."
          }
        ],
        "security": [
          {
            "managementKey": []
          }
        ],
        "summary": "Read one saved audience",
        "description": "Needs `segment.read`. Costs one budget unit. The body is a bare segment object, not wrapped. A guessed id and another account's id are indistinguishable from a deleted one: the lookup is scoped by account, so all three are 404.",
        "responses": {
          "200": {
            "description": "The segment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SavedSegment"
                },
                "examples": {
                  "one": {
                    "value": {
                      "id": 12,
                      "name": "تهرانی‌ها",
                      "kind": "dynamic",
                      "definition": {
                        "version": 1,
                        "root": {
                          "kind": "group",
                          "op": "and",
                          "children": [
                            {
                              "kind": "trait",
                              "trait": "city",
                              "operator": "eq",
                              "value": {
                                "type": "string",
                                "str": "تهران"
                              }
                            }
                          ]
                        }
                      },
                      "description_fa": "کاربرانی که شهرشان تهران است",
                      "last_size": 61432,
                      "last_computed_at": "2026-08-06T09:00:00Z",
                      "updated_at": "2026-08-06T09:00:00Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The id is not a positive integer. Flat shape on this route.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FlatError"
                },
                "examples": {
                  "badId": {
                    "value": {
                      "error": "invalid segment id"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential, an unresolvable one, an expired key, a `wk_` write key, or a dashboard session. Four distinct codes: `unauthenticated`, `key_expired`, `write_key_rejected`, `api_key_required`. Sessions are refused outright so this surface has no ambient credential at all, which means there is nothing to forge a request with.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "error": {
                        "code": "unauthenticated",
                        "message": "a valid API key is required"
                      }
                    }
                  },
                  "wrongFamily": {
                    "value": {
                      "error": {
                        "code": "write_key_rejected",
                        "message": "that is an SDK write key (wk_…); this API needs a management key (sk_seg_…)"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key's role, intersected with its scopes, does not carry the permission. `need` names it exactly. Do not retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "forbidden": {
                    "value": {
                      "error": {
                        "code": "forbidden",
                        "message": "this key does not carry segment.read, see GET /v1/whoami for what it does carry",
                        "need": "segment.read"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such segment on this account. Flat shape.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FlatError"
                },
                "examples": {
                  "notFound": {
                    "value": {
                      "error": "segment not found"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "The key has spent its weighted request budget for the calendar minute. 600 units by default, which is roughly two heavy reports a minute or six hundred cheap ones. The budget is per key, not per account, so a runaway agent cannot exhaust the budget your order pipeline depends on. There are no X-RateLimit headers on this path.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 60. Conservative: the fixed minute may roll sooner."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "exhausted": {
                    "value": {
                      "error": {
                        "code": "budget_exhausted",
                        "message": "this key has spent its request budget for the minute"
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The budget could not be verified. This fails closed, unlike the transactional rate limiter, because an unmetered agent in a loop is the more expensive failure and nobody's checkout breaks because a report waited. A Redis outage therefore takes this whole surface down, including whoami and capabilities. No Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "budgetDown": {
                    "value": {
                      "error": {
                        "code": "budget_unavailable",
                        "message": "could not verify the request budget"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "updateSegment",
        "tags": [
          "management",
          "segments"
        ],
        "servers": [
          {
            "url": "https://api.segmentic.net",
            "description": "Management. Reads and writes audiences, campaigns, reports and exports, and sends one transactional message at a time. Authenticated by a secret key that carries permissions. There is no CORS preflight handler on this mux, so a browser cannot call it."
          }
        ],
        "security": [
          {
            "managementKey": []
          }
        ],
        "summary": "Replace a saved audience",
        "description": "Needs `segment.write`. Costs one budget unit. A whole-object replace, not a merge. There is no PATCH on this surface, no If-Match and no version token of any kind, so two concurrent writers silently clobber each other. A blank or omitted `name` keeps the existing name. It does not clear it and it is not an error. The filter is validated before the existence check, so an invalid filter is 422 even for a segment that does not exist. The segment is read first, so an id from another account's URL is a 404 rather than a write that silently creates a segment on your own.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SegmentWriteRequest"
              },
              "examples": {
                "widen": {
                  "value": {
                    "name": "تهرانی‌ها و کرجی‌ها",
                    "definition": {
                      "version": 1,
                      "root": {
                        "kind": "group",
                        "op": "or",
                        "children": [
                          {
                            "kind": "trait",
                            "trait": "city",
                            "operator": "in",
                            "value": {
                              "type": "list",
                              "list": [
                                "تهران",
                                "کرج"
                              ]
                            }
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Replaced.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SegmentWriteResponse"
                },
                "examples": {
                  "updated": {
                    "value": {
                      "id": 12,
                      "name": "تهرانی‌ها و کرجی‌ها",
                      "description_fa": "کاربرانی که شهرشان یکی از تهران، کرج است"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, or an id that is not a positive integer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "badId": {
                    "value": {
                      "error": {
                        "code": "bad_id",
                        "message": "the path must carry a positive integer id"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential, an unresolvable one, an expired key, a `wk_` write key, or a dashboard session. Four distinct codes: `unauthenticated`, `key_expired`, `write_key_rejected`, `api_key_required`. Sessions are refused outright so this surface has no ambient credential at all, which means there is nothing to forge a request with.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "error": {
                        "code": "unauthenticated",
                        "message": "a valid API key is required"
                      }
                    }
                  },
                  "wrongFamily": {
                    "value": {
                      "error": {
                        "code": "write_key_rejected",
                        "message": "that is an SDK write key (wk_…); this API needs a management key (sk_seg_…)"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key's role, intersected with its scopes, does not carry the permission. `need` names it exactly. Do not retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "forbidden": {
                    "value": {
                      "error": {
                        "code": "forbidden",
                        "message": "this key does not carry segment.write, see GET /v1/whoami for what it does carry",
                        "need": "segment.write"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such segment. An update is never a create.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "notFound": {
                    "value": {
                      "error": {
                        "code": "not_found",
                        "message": "no such segment"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "The filter does not compile.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "badFilter": {
                    "value": {
                      "error": {
                        "code": "filter_invalid",
                        "message": "segment: group with no children"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "The key has spent its weighted request budget for the calendar minute. 600 units by default, which is roughly two heavy reports a minute or six hundred cheap ones. The budget is per key, not per account, so a runaway agent cannot exhaust the budget your order pipeline depends on. There are no X-RateLimit headers on this path.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 60. Conservative: the fixed minute may roll sooner."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "exhausted": {
                    "value": {
                      "error": {
                        "code": "budget_exhausted",
                        "message": "this key has spent its request budget for the minute"
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The budget could not be verified. This fails closed, unlike the transactional rate limiter, because an unmetered agent in a loop is the more expensive failure and nobody's checkout breaks because a report waited. A Redis outage therefore takes this whole surface down, including whoami and capabilities. No Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "budgetDown": {
                    "value": {
                      "error": {
                        "code": "budget_unavailable",
                        "message": "could not verify the request budget"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteSegment",
        "tags": [
          "management",
          "segments"
        ],
        "servers": [
          {
            "url": "https://api.segmentic.net",
            "description": "Management. Reads and writes audiences, campaigns, reports and exports, and sends one transactional message at a time. Authenticated by a secret key that carries permissions. There is no CORS preflight handler on this mux, so a browser cannot call it."
          }
        ],
        "security": [
          {
            "managementKey": []
          }
        ],
        "summary": "Delete a saved audience",
        "description": "Needs `segment.delete`, its own permission, because removing an audience somebody's journey references is not the same act as editing one. Costs one budget unit. There is no 404 for an unknown id: the delete is unconditional and a no-op answers 204. There is no in-use refusal either, so deleting an audience a scheduled campaign points at is one unremarkable call.",
        "responses": {
          "204": {
            "description": "Gone, or was never there. Empty body."
          },
          "400": {
            "description": "The id is not a positive integer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "badId": {
                    "value": {
                      "error": {
                        "code": "bad_id",
                        "message": "the path must carry a positive integer id"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential, an unresolvable one, an expired key, a `wk_` write key, or a dashboard session. Four distinct codes: `unauthenticated`, `key_expired`, `write_key_rejected`, `api_key_required`. Sessions are refused outright so this surface has no ambient credential at all, which means there is nothing to forge a request with.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "error": {
                        "code": "unauthenticated",
                        "message": "a valid API key is required"
                      }
                    }
                  },
                  "wrongFamily": {
                    "value": {
                      "error": {
                        "code": "write_key_rejected",
                        "message": "that is an SDK write key (wk_…); this API needs a management key (sk_seg_…)"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key's role, intersected with its scopes, does not carry the permission. `need` names it exactly. Do not retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "forbidden": {
                    "value": {
                      "error": {
                        "code": "forbidden",
                        "message": "this key does not carry segment.delete, see GET /v1/whoami for what it does carry",
                        "need": "segment.delete"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "The key has spent its weighted request budget for the calendar minute. 600 units by default, which is roughly two heavy reports a minute or six hundred cheap ones. The budget is per key, not per account, so a runaway agent cannot exhaust the budget your order pipeline depends on. There are no X-RateLimit headers on this path.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 60. Conservative: the fixed minute may roll sooner."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "exhausted": {
                    "value": {
                      "error": {
                        "code": "budget_exhausted",
                        "message": "this key has spent its request budget for the minute"
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The budget could not be verified. This fails closed, unlike the transactional rate limiter, because an unmetered agent in a loop is the more expensive failure and nobody's checkout breaks because a report waited. A Redis outage therefore takes this whole surface down, including whoami and capabilities. No Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "budgetDown": {
                    "value": {
                      "error": {
                        "code": "budget_unavailable",
                        "message": "could not verify the request budget"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/campaigns": {
      "get": {
        "operationId": "listCampaigns",
        "tags": [
          "management",
          "campaigns"
        ],
        "servers": [
          {
            "url": "https://api.segmentic.net",
            "description": "Management. Reads and writes audiences, campaigns, reports and exports, and sends one transactional message at a time. Authenticated by a secret key that carries permissions. There is no CORS preflight handler on this mux, so a browser cannot call it."
          }
        ],
        "security": [
          {
            "managementKey": []
          }
        ],
        "summary": "List campaigns",
        "description": "Needs `campaign.read`. Costs one budget unit. Not paginated: `limit` and `cursor` are ignored, and the query underneath ends in LIMIT 200 ordered by `updated_at` descending, exactly as on /v1/segments. The two hundred and first campaign is absent with nothing saying so. `status` is one of draft, scheduled, running, paused, completed, cancelled, failed.",
        "responses": {
          "200": {
            "description": "Every campaign. The array is `[]`, never null.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignListResponse"
                },
                "examples": {
                  "one": {
                    "value": {
                      "campaigns": [
                        {
                          "id": 5,
                          "name": "پوش نوروز",
                          "channel": "push",
                          "status": "draft",
                          "estimated": 61432,
                          "processed": 0,
                          "sent": 0,
                          "scheduled_at": "2026-03-20T06:00:00Z",
                          "updated_at": "2026-08-06T09:00:00Z"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential, an unresolvable one, an expired key, a `wk_` write key, or a dashboard session. Four distinct codes: `unauthenticated`, `key_expired`, `write_key_rejected`, `api_key_required`. Sessions are refused outright so this surface has no ambient credential at all, which means there is nothing to forge a request with.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "error": {
                        "code": "unauthenticated",
                        "message": "a valid API key is required"
                      }
                    }
                  },
                  "wrongFamily": {
                    "value": {
                      "error": {
                        "code": "write_key_rejected",
                        "message": "that is an SDK write key (wk_…); this API needs a management key (sk_seg_…)"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key's role, intersected with its scopes, does not carry the permission. `need` names it exactly. Do not retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "forbidden": {
                    "value": {
                      "error": {
                        "code": "forbidden",
                        "message": "this key does not carry campaign.read, see GET /v1/whoami for what it does carry",
                        "need": "campaign.read"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "The key has spent its weighted request budget for the calendar minute. 600 units by default, which is roughly two heavy reports a minute or six hundred cheap ones. The budget is per key, not per account, so a runaway agent cannot exhaust the budget your order pipeline depends on. There are no X-RateLimit headers on this path.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 60. Conservative: the fixed minute may roll sooner."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "exhausted": {
                    "value": {
                      "error": {
                        "code": "budget_exhausted",
                        "message": "this key has spent its request budget for the minute"
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The budget could not be verified. This fails closed, unlike the transactional rate limiter, because an unmetered agent in a loop is the more expensive failure and nobody's checkout breaks because a report waited. A Redis outage therefore takes this whole surface down, including whoami and capabilities. No Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "budgetDown": {
                    "value": {
                      "error": {
                        "code": "budget_unavailable",
                        "message": "could not verify the request budget"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createCampaign",
        "tags": [
          "management",
          "campaigns"
        ],
        "servers": [
          {
            "url": "https://api.segmentic.net",
            "description": "Management. Reads and writes audiences, campaigns, reports and exports, and sends one transactional message at a time. Authenticated by a secret key that carries permissions. There is no CORS preflight handler on this mux, so a browser cannot call it."
          }
        ],
        "security": [
          {
            "managementKey": []
          }
        ],
        "summary": "Create a campaign, as a draft",
        "description": "Needs `campaign.write`. Costs one budget unit. The status is forced to `draft` whatever you send. Creating and sending are two calls on two permissions, exactly as they are two buttons in the dashboard: a single create-and-send would collapse the reversible act into the irreversible one. `template_id` is required. Inline message bodies are not accepted. Either `segment_id` or an inline `definition` names the audience, and the inline definition is not validated here, unlike a segment's. Three fields fail quietly and are worth guarding in your own code: `name` is not validated at all, so an empty campaign name is accepted; an unparseable `scheduled_at` is dropped rather than refused, leaving the campaign unscheduled; and `priority` and `min_interval_hours` cannot be set from this route at all.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CampaignCreateRequest"
              },
              "examples": {
                "scheduledPush": {
                  "value": {
                    "name": "پوش نوروز",
                    "channel": "push",
                    "template_id": 3,
                    "segment_id": 12,
                    "scheduled_at": "2026-03-20T06:00:00Z",
                    "control_group_pct": 5,
                    "goal_event": "order_completed"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created as a draft.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignCreateResponse"
                },
                "examples": {
                  "created": {
                    "value": {
                      "id": 5,
                      "status": "draft"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, or a channel that does not parse. A wrong channel that reported success would be worse than a 400 naming the field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "badChannel": {
                    "value": {
                      "error": {
                        "code": "invalid_channel",
                        "message": "unknown channel \"bogus\"",
                        "details": {
                          "field": "channel"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential, an unresolvable one, an expired key, a `wk_` write key, or a dashboard session. Four distinct codes: `unauthenticated`, `key_expired`, `write_key_rejected`, `api_key_required`. Sessions are refused outright so this surface has no ambient credential at all, which means there is nothing to forge a request with.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "error": {
                        "code": "unauthenticated",
                        "message": "a valid API key is required"
                      }
                    }
                  },
                  "wrongFamily": {
                    "value": {
                      "error": {
                        "code": "write_key_rejected",
                        "message": "that is an SDK write key (wk_…); this API needs a management key (sk_seg_…)"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key's role, intersected with its scopes, does not carry the permission. `need` names it exactly. Do not retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "forbidden": {
                    "value": {
                      "error": {
                        "code": "forbidden",
                        "message": "this key does not carry campaign.write, see GET /v1/whoami for what it does carry",
                        "need": "campaign.write"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "The campaign does not validate. The message is the validator's own text.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "noTemplate": {
                    "value": {
                      "error": {
                        "code": "campaign_invalid",
                        "message": "campaign: a template is required"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "The key has spent its weighted request budget for the calendar minute. 600 units by default, which is roughly two heavy reports a minute or six hundred cheap ones. The budget is per key, not per account, so a runaway agent cannot exhaust the budget your order pipeline depends on. There are no X-RateLimit headers on this path.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 60. Conservative: the fixed minute may roll sooner."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "exhausted": {
                    "value": {
                      "error": {
                        "code": "budget_exhausted",
                        "message": "this key has spent its request budget for the minute"
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The budget could not be verified. This fails closed, unlike the transactional rate limiter, because an unmetered agent in a loop is the more expensive failure and nobody's checkout breaks because a report waited. A Redis outage therefore takes this whole surface down, including whoami and capabilities. No Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "budgetDown": {
                    "value": {
                      "error": {
                        "code": "budget_unavailable",
                        "message": "could not verify the request budget"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/campaigns/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "format": "int64",
            "minimum": 1
          }
        }
      ],
      "get": {
        "operationId": "campaignReport",
        "tags": [
          "management",
          "campaigns"
        ],
        "servers": [
          {
            "url": "https://api.segmentic.net",
            "description": "Management. Reads and writes audiences, campaigns, reports and exports, and sends one transactional message at a time. Authenticated by a secret key that carries permissions. There is no CORS preflight handler on this mux, so a browser cannot call it."
          }
        ],
        "security": [
          {
            "managementKey": []
          }
        ],
        "summary": "Read a campaign and its whole report",
        "description": "Needs `campaign.read`. Costs five budget units. This is the report, not just the record. `reach` answers the question this kind of platform is asked constantly and usually cannot: the segment said sixty thousand, why did forty-one thousand receive it. Every engagement rate ships as a numerator and a named denominator rather than a percentage, so you always hold both operands. A single blended percentage is the number you cannot reproduce. `uplift` is absent until the attribution window closes, because a figure computed while conversions can still arrive is one that moves. `reach`, `delivery`, `engagement` and `uplift` are best-effort: a warehouse blip costs the section, never the response.",
        "responses": {
          "200": {
            "description": "The campaign, its progress and its report sections.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignReport"
                },
                "examples": {
                  "finished": {
                    "value": {
                      "campaign": {
                        "id": 5,
                        "tenant_id": 7,
                        "name": "پوش نوروز",
                        "segment_id": 12,
                        "definition": {
                          "version": 1,
                          "root": {
                            "kind": "group",
                            "op": "and"
                          }
                        },
                        "template_id": 3,
                        "channel": "sms",
                        "status": "completed",
                        "scheduled_at": "2026-03-20T06:00:00Z",
                        "control_group_pct": 5,
                        "goal_event": "order_completed"
                      },
                      "progress": {
                        "campaign_id": 5,
                        "cursor": "u-98213",
                        "estimated": 61432,
                        "processed": 61432,
                        "sent": 58210,
                        "suppressed": 1802,
                        "deferred": 0,
                        "failed": 1420,
                        "holdout": 0,
                        "started_at": "2026-03-20T06:00:00Z",
                        "updated_at": "2026-03-20T06:41:00Z",
                        "finished_at": "2026-03-20T06:41:00Z"
                      },
                      "percent": 100,
                      "reach": [
                        {
                          "status": "sent",
                          "reason": "",
                          "reason_fa": "",
                          "count": 58210
                        },
                        {
                          "status": "withheld",
                          "reason": "opted_out",
                          "reason_fa": "لغو اشتراک کرده است",
                          "count": 1802
                        }
                      ],
                      "delivery": [
                        {
                          "delivery": "delivered",
                          "delivery_fa": "رسیده",
                          "count": 55120
                        }
                      ],
                      "engagement": [
                        {
                          "channel": "sms",
                          "channel_fa": "پیامک",
                          "issued": 58210,
                          "withheld": 0,
                          "measurable_open": 0,
                          "measurable_click": 58210,
                          "opened": 0,
                          "clicked": 4102,
                          "opened_unmeasurable": 0,
                          "clicked_unmeasurable": 0,
                          "why_open": "پیامک باز شدن را گزارش نمی‌کند",
                          "why_click": ""
                        }
                      ],
                      "engagement_rejects": [
                        {
                          "verdict": "bad_signature",
                          "verdict_fa": "امضا نامعتبر",
                          "detail": "",
                          "count": 12
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The id is not a positive integer. Flat shape.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FlatError"
                },
                "examples": {
                  "badId": {
                    "value": {
                      "error": "invalid campaign id"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential, an unresolvable one, an expired key, a `wk_` write key, or a dashboard session. Four distinct codes: `unauthenticated`, `key_expired`, `write_key_rejected`, `api_key_required`. Sessions are refused outright so this surface has no ambient credential at all, which means there is nothing to forge a request with.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "error": {
                        "code": "unauthenticated",
                        "message": "a valid API key is required"
                      }
                    }
                  },
                  "wrongFamily": {
                    "value": {
                      "error": {
                        "code": "write_key_rejected",
                        "message": "that is an SDK write key (wk_…); this API needs a management key (sk_seg_…)"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key's role, intersected with its scopes, does not carry the permission. `need` names it exactly. Do not retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "forbidden": {
                    "value": {
                      "error": {
                        "code": "forbidden",
                        "message": "this key does not carry campaign.read, see GET /v1/whoami for what it does carry",
                        "need": "campaign.read"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such campaign on this account. Flat shape.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FlatError"
                },
                "examples": {
                  "notFound": {
                    "value": {
                      "error": "campaign not found"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "The key has spent its weighted request budget for the calendar minute. 600 units by default, which is roughly two heavy reports a minute or six hundred cheap ones. The budget is per key, not per account, so a runaway agent cannot exhaust the budget your order pipeline depends on. There are no X-RateLimit headers on this path.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 60. Conservative: the fixed minute may roll sooner."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "exhausted": {
                    "value": {
                      "error": {
                        "code": "budget_exhausted",
                        "message": "this key has spent its request budget for the minute"
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The budget could not be verified. This fails closed, unlike the transactional rate limiter, because an unmetered agent in a loop is the more expensive failure and nobody's checkout breaks because a report waited. A Redis outage therefore takes this whole surface down, including whoami and capabilities. No Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "budgetDown": {
                    "value": {
                      "error": {
                        "code": "budget_unavailable",
                        "message": "could not verify the request budget"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/campaigns/{id}/recurrence": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "format": "int64",
            "minimum": 1
          }
        }
      ],
      "put": {
        "operationId": "setCampaignRecurrence",
        "tags": [
          "management",
          "campaigns"
        ],
        "servers": [
          {
            "url": "https://api.segmentic.net",
            "description": "Management. Reads and writes audiences, campaigns, reports and exports, and sends one transactional message at a time. Authenticated by a secret key that carries permissions. There is no CORS preflight handler on this mux, so a browser cannot call it."
          }
        ],
        "security": [
          {
            "managementKey": []
          }
        ],
        "summary": "Start or replace a campaign repeat schedule",
        "description": "Needs `campaign.send`. Costs one budget unit. Every occurrence is a new campaign with the saved campaign's audience and content. All hours are Tehran time, weekdays start with Saturday as zero, and monthly days use the Jalali calendar. The worker owns the occurrence counter, so a value supplied by a client is reset to zero.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CampaignRecurrenceRequest"
              },
              "examples": {
                "weekly": {
                  "value": {
                    "recurrence": {
                      "cadence": "weekly",
                      "hour": 9,
                      "day_of_week": 0,
                      "max_occurrences": 4
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The schedule was saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignRecurrenceResponse"
                },
                "examples": {
                  "saved": {
                    "value": {
                      "status": "ok"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The id or JSON body could not be read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "badId": {
                    "value": {
                      "error": {
                        "code": "bad_id",
                        "message": "the path must carry a positive integer id"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential, an unresolvable one, an expired key, a `wk_` write key, or a dashboard session. Four distinct codes: `unauthenticated`, `key_expired`, `write_key_rejected`, `api_key_required`. Sessions are refused outright so this surface has no ambient credential at all, which means there is nothing to forge a request with.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "error": {
                        "code": "unauthenticated",
                        "message": "a valid API key is required"
                      }
                    }
                  },
                  "wrongFamily": {
                    "value": {
                      "error": {
                        "code": "write_key_rejected",
                        "message": "that is an SDK write key (wk_…); this API needs a management key (sk_seg_…)"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key's role, intersected with its scopes, does not carry the permission. `need` names it exactly. Do not retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "forbidden": {
                    "value": {
                      "error": {
                        "code": "forbidden",
                        "message": "this key does not carry campaign.send, see GET /v1/whoami for what it does carry",
                        "need": "campaign.send"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "The schedule has an invalid cadence, hour, weekday or month day.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "invalid": {
                    "value": {
                      "error": {
                        "code": "recurrence_invalid",
                        "message": "campaign: unknown recurrence cadence"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "The key has spent its weighted request budget for the calendar minute. 600 units by default, which is roughly two heavy reports a minute or six hundred cheap ones. The budget is per key, not per account, so a runaway agent cannot exhaust the budget your order pipeline depends on. There are no X-RateLimit headers on this path.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 60. Conservative: the fixed minute may roll sooner."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "exhausted": {
                    "value": {
                      "error": {
                        "code": "budget_exhausted",
                        "message": "this key has spent its request budget for the minute"
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The budget could not be verified. This fails closed, unlike the transactional rate limiter, because an unmetered agent in a loop is the more expensive failure and nobody's checkout breaks because a report waited. A Redis outage therefore takes this whole surface down, including whoami and capabilities. No Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "budgetDown": {
                    "value": {
                      "error": {
                        "code": "budget_unavailable",
                        "message": "could not verify the request budget"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "clearCampaignRecurrence",
        "tags": [
          "management",
          "campaigns"
        ],
        "servers": [
          {
            "url": "https://api.segmentic.net",
            "description": "Management. Reads and writes audiences, campaigns, reports and exports, and sends one transactional message at a time. Authenticated by a secret key that carries permissions. There is no CORS preflight handler on this mux, so a browser cannot call it."
          }
        ],
        "security": [
          {
            "managementKey": []
          }
        ],
        "summary": "Stop future automatic campaign repeats",
        "description": "Needs `campaign.send`. Costs one budget unit. Campaigns already created by the schedule are not changed or deleted.",
        "responses": {
          "200": {
            "description": "Future repeats were stopped.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignRecurrenceResponse"
                },
                "examples": {
                  "cleared": {
                    "value": {
                      "status": "ok",
                      "note": "campaigns already created by this schedule are unchanged"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The id is not a positive integer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "badId": {
                    "value": {
                      "error": {
                        "code": "bad_id",
                        "message": "the path must carry a positive integer id"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential, an unresolvable one, an expired key, a `wk_` write key, or a dashboard session. Four distinct codes: `unauthenticated`, `key_expired`, `write_key_rejected`, `api_key_required`. Sessions are refused outright so this surface has no ambient credential at all, which means there is nothing to forge a request with.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "error": {
                        "code": "unauthenticated",
                        "message": "a valid API key is required"
                      }
                    }
                  },
                  "wrongFamily": {
                    "value": {
                      "error": {
                        "code": "write_key_rejected",
                        "message": "that is an SDK write key (wk_…); this API needs a management key (sk_seg_…)"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key's role, intersected with its scopes, does not carry the permission. `need` names it exactly. Do not retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "forbidden": {
                    "value": {
                      "error": {
                        "code": "forbidden",
                        "message": "this key does not carry campaign.send, see GET /v1/whoami for what it does carry",
                        "need": "campaign.send"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "The key has spent its weighted request budget for the calendar minute. 600 units by default, which is roughly two heavy reports a minute or six hundred cheap ones. The budget is per key, not per account, so a runaway agent cannot exhaust the budget your order pipeline depends on. There are no X-RateLimit headers on this path.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 60. Conservative: the fixed minute may roll sooner."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "exhausted": {
                    "value": {
                      "error": {
                        "code": "budget_exhausted",
                        "message": "this key has spent its request budget for the minute"
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The budget could not be verified. This fails closed, unlike the transactional rate limiter, because an unmetered agent in a loop is the more expensive failure and nobody's checkout breaks because a report waited. A Redis outage therefore takes this whole surface down, including whoami and capabilities. No Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "budgetDown": {
                    "value": {
                      "error": {
                        "code": "budget_unavailable",
                        "message": "could not verify the request budget"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/campaigns/{id}/send": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "format": "int64",
            "minimum": 1
          }
        }
      ],
      "post": {
        "operationId": "sendCampaign",
        "tags": [
          "management",
          "campaigns"
        ],
        "servers": [
          {
            "url": "https://api.segmentic.net",
            "description": "Management. Reads and writes audiences, campaigns, reports and exports, and sends one transactional message at a time. Authenticated by a secret key that carries permissions. There is no CORS preflight handler on this mux, so a browser cannot call it."
          }
        ],
        "security": [
          {
            "managementKey": []
          }
        ],
        "summary": "Schedule a campaign to send",
        "description": "Needs `campaign.send`. Costs one budget unit. No request body is read. This is the irreversible call. There is no way to pause, resume or cancel a campaign on this surface. The dashboard has all three; this API has none of them. Once your backend schedules a send, only a person in the panel can stop it. When your account requires approval, the gate runs first and fails closed: an approval store that cannot answer refuses the send, because a send is irreversible and the campaign is still there in five minutes. `approval_stale` is a separate code from `approval_required` because the two send an integration to different places: one to 'ask for approval', the other to 'somebody edited this after it was approved'.",
        "responses": {
          "202": {
            "description": "Scheduled. The worker picks it up and asks the approval question again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignCreateResponse"
                },
                "examples": {
                  "scheduled": {
                    "value": {
                      "id": 5,
                      "status": "scheduled"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The id is not a positive integer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "badId": {
                    "value": {
                      "error": {
                        "code": "bad_id",
                        "message": "the path must carry a positive integer id"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential, an unresolvable one, an expired key, a `wk_` write key, or a dashboard session. Four distinct codes: `unauthenticated`, `key_expired`, `write_key_rejected`, `api_key_required`. Sessions are refused outright so this surface has no ambient credential at all, which means there is nothing to forge a request with.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "error": {
                        "code": "unauthenticated",
                        "message": "a valid API key is required"
                      }
                    }
                  },
                  "wrongFamily": {
                    "value": {
                      "error": {
                        "code": "write_key_rejected",
                        "message": "that is an SDK write key (wk_…); this API needs a management key (sk_seg_…)"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key's role, intersected with its scopes, does not carry the permission. `need` names it exactly. Do not retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "forbidden": {
                    "value": {
                      "error": {
                        "code": "forbidden",
                        "message": "this key does not carry campaign.send, see GET /v1/whoami for what it does carry",
                        "need": "campaign.send"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such campaign on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "notFound": {
                    "value": {
                      "error": {
                        "code": "not_found",
                        "message": "no such campaign"
                      }
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "A person has to act in the panel. Retrying immediately achieves nothing, and there is no route here that reports the answer: you learn it by retrying the send and reading this code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "needsApproval": {
                    "value": {
                      "error": {
                        "code": "approval_required",
                        "message": "this account requires a campaign to be approved before it sends"
                      }
                    }
                  },
                  "editedAfterApproval": {
                    "value": {
                      "error": {
                        "code": "approval_stale",
                        "message": "the campaign changed after it was approved; submit it again"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "The key has spent its weighted request budget for the calendar minute. 600 units by default, which is roughly two heavy reports a minute or six hundred cheap ones. The budget is per key, not per account, so a runaway agent cannot exhaust the budget your order pipeline depends on. There are no X-RateLimit headers on this path.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 60. Conservative: the fixed minute may roll sooner."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "exhausted": {
                    "value": {
                      "error": {
                        "code": "budget_exhausted",
                        "message": "this key has spent its request budget for the minute"
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The budget could not be verified. This fails closed, unlike the transactional rate limiter, because an unmetered agent in a loop is the more expensive failure and nobody's checkout breaks because a report waited. A Redis outage therefore takes this whole surface down, including whoami and capabilities. No Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "budgetDown": {
                    "value": {
                      "error": {
                        "code": "budget_unavailable",
                        "message": "could not verify the request budget"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/campaigns/{id}/submit": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "format": "int64",
            "minimum": 1
          }
        }
      ],
      "post": {
        "operationId": "submitCampaign",
        "tags": [
          "management",
          "campaigns"
        ],
        "servers": [
          {
            "url": "https://api.segmentic.net",
            "description": "Management. Reads and writes audiences, campaigns, reports and exports, and sends one transactional message at a time. Authenticated by a secret key that carries permissions. There is no CORS preflight handler on this mux, so a browser cannot call it."
          }
        ],
        "security": [
          {
            "managementKey": []
          }
        ],
        "summary": "Submit a campaign for approval",
        "description": "Needs `campaign.write`, not `campaign.approve`: the author is asking, not deciding. Costs one budget unit. No request body. Served only when this deployment has approvals configured; check `campaign_approval` in GET /v1/capabilities. The submission is attributed to the key's creator, not to the key. The two-person rule is about people, and attributing a request to 'api key 12' would let one person hold both halves by minting a key. `fingerprint` covers the audience, the template, the channel and the sorted variant list, so a split added after approval invalidates it. There is no route here to approve, to list the queue, or to read approval history. All three are in the panel only.",
        "responses": {
          "202": {
            "description": "Submitted, awaiting a human.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignSubmitResponse"
                },
                "examples": {
                  "pending": {
                    "value": {
                      "approval_id": 88,
                      "state": "pending",
                      "fingerprint": "3f1a9c02b7de4415aa0e8c1d2f6b3790"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The id is not a positive integer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "badId": {
                    "value": {
                      "error": {
                        "code": "bad_id",
                        "message": "the path must carry a positive integer id"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential, an unresolvable one, an expired key, a `wk_` write key, or a dashboard session. Four distinct codes: `unauthenticated`, `key_expired`, `write_key_rejected`, `api_key_required`. Sessions are refused outright so this surface has no ambient credential at all, which means there is nothing to forge a request with.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "error": {
                        "code": "unauthenticated",
                        "message": "a valid API key is required"
                      }
                    }
                  },
                  "wrongFamily": {
                    "value": {
                      "error": {
                        "code": "write_key_rejected",
                        "message": "that is an SDK write key (wk_…); this API needs a management key (sk_seg_…)"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key's role, intersected with its scopes, does not carry the permission. `need` names it exactly. Do not retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "forbidden": {
                    "value": {
                      "error": {
                        "code": "forbidden",
                        "message": "this key does not carry campaign.write, see GET /v1/whoami for what it does carry",
                        "need": "campaign.write"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such campaign on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "notFound": {
                    "value": {
                      "error": {
                        "code": "not_found",
                        "message": "no such campaign"
                      }
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "Only a draft or a paused campaign can be submitted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "wrongStatus": {
                    "value": {
                      "error": {
                        "code": "not_submittable",
                        "message": "only a draft or paused campaign can be submitted for approval"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "The campaign does not validate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "invalid": {
                    "value": {
                      "error": {
                        "code": "campaign_invalid",
                        "message": "campaign: a template is required"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "The key has spent its weighted request budget for the calendar minute. 600 units by default, which is roughly two heavy reports a minute or six hundred cheap ones. The budget is per key, not per account, so a runaway agent cannot exhaust the budget your order pipeline depends on. There are no X-RateLimit headers on this path.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 60. Conservative: the fixed minute may roll sooner."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "exhausted": {
                    "value": {
                      "error": {
                        "code": "budget_exhausted",
                        "message": "this key has spent its request budget for the minute"
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The budget could not be verified. This fails closed, unlike the transactional rate limiter, because an unmetered agent in a loop is the more expensive failure and nobody's checkout breaks because a report waited. A Redis outage therefore takes this whole surface down, including whoami and capabilities. No Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "budgetDown": {
                    "value": {
                      "error": {
                        "code": "budget_unavailable",
                        "message": "could not verify the request budget"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/events": {
      "post": {
        "operationId": "ingestEvents",
        "tags": [
          "management",
          "events"
        ],
        "servers": [
          {
            "url": "https://api.segmentic.net",
            "description": "Management. Reads and writes audiences, campaigns, reports and exports, and sends one transactional message at a time. Authenticated by a secret key that carries permissions. There is no CORS preflight handler on this mux, so a browser cannot call it."
          }
        ],
        "security": [
          {
            "managementKey": []
          }
        ],
        "summary": "Send events from your own backend",
        "description": "Needs `profile.write`, which is what this does: it writes to people's profiles and their event history. Inventing a second name for the same capability would let somebody grant one believing they withheld the other. Costs five budget units. Body cap 8 MiB, up to 500 events. Not the same endpoint as the collector's /v1/batch, and four differences will cost you if you assume otherwise. The array key is `events`, not `batch`. There is no batch-level `context` or `sent_at`; every item carries its own. There is no de-duplication by `message_id` here, so a retried batch double-counts. And warnings are discarded, so a missing `message_id` is silently generated and you are never told. No IP and no User-Agent are read from the request, deliberately: this is a server-to-server call, so the address belongs to your data centre, and attributing a recipient's city from it would put every one of your users in one place. There is no geo enrichment on this door. A timestamp older than 30 days is silently clamped to exactly 30-days-ago, not refused. Do not migrate history through this endpoint. The collector honours your account's own retention window; this handler does not fill it in.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/IngestEventsRequest"
              },
              "examples": {
                "capturedPayment": {
                  "value": {
                    "events": [
                      {
                        "type": "track",
                        "event": "order_completed",
                        "user_id": "u-1",
                        "message_id": "srv-order-8821",
                        "timestamp": "2026-08-06T09:12:41Z",
                        "properties": {
                          "revenue": 480000,
                          "order_id": "8821",
                          "currency": "IRR"
                        },
                        "context": {
                          "locale": "fa-IR",
                          "timezone": "Asia/Tehran"
                        }
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Queued, not stored. They are in the same pipeline an SDK's events go through and become queryable seconds later. 200 would invite you to read them back immediately and conclude they were lost. `rejected[].reason` is the full error text, and two of the sentinels are wrapped with the value you sent, so match on the prefix or split on ': ' rather than on equality.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestEventsResponse"
                },
                "examples": {
                  "allAccepted": {
                    "value": {
                      "accepted": 2
                    }
                  },
                  "partial": {
                    "value": {
                      "accepted": 2,
                      "rejected": [
                        {
                          "index": 1,
                          "reason": "missing_identity"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, or `events` absent or empty.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "empty": {
                    "value": {
                      "error": {
                        "code": "batch_empty",
                        "message": "events must contain at least one item"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential, an unresolvable one, an expired key, a `wk_` write key, or a dashboard session. Four distinct codes: `unauthenticated`, `key_expired`, `write_key_rejected`, `api_key_required`. Sessions are refused outright so this surface has no ambient credential at all, which means there is nothing to forge a request with.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "error": {
                        "code": "unauthenticated",
                        "message": "a valid API key is required"
                      }
                    }
                  },
                  "wrongFamily": {
                    "value": {
                      "error": {
                        "code": "write_key_rejected",
                        "message": "that is an SDK write key (wk_…); this API needs a management key (sk_seg_…)"
                      }
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "Over quota, cancelled, or out of trial. The whole batch is refused: a partial accept would leave you unable to tell what to resend. The message is Persian, always, because this surface has no locale middleware. Do not retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "eventCap": {
                    "value": {
                      "error": {
                        "code": "quota_event_cap",
                        "message": "سقف رویدادهای این ماه پر شده است"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key's role, intersected with its scopes, does not carry the permission. `need` names it exactly. Do not retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "forbidden": {
                    "value": {
                      "error": {
                        "code": "forbidden",
                        "message": "this key does not carry profile.write, see GET /v1/whoami for what it does carry",
                        "need": "profile.write"
                      }
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "More than 500 events. The limit is published in the refusal, so a client sizing its loop does not have to discover it by bisection.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "tooLarge": {
                    "value": {
                      "error": {
                        "code": "batch_too_large",
                        "message": "a batch may carry at most 500 events",
                        "details": {
                          "limit": 500,
                          "sent": 501
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Not one event survived validation. `details` carries the per-index reasons. Every item is validated here, before the sink sees it: the sink behind this drops what it cannot normalise, which is right for an importer that pre-validated its own rows and wrong for an endpoint taking arbitrary JSON from the internet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "allRejected": {
                    "value": {
                      "error": {
                        "code": "all_events_rejected",
                        "message": "no event in this batch could be accepted",
                        "details": [
                          {
                            "index": 0,
                            "reason": "missing_identity"
                          },
                          {
                            "index": 1,
                            "reason": "unknown_type: \"trak\""
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "The key has spent its weighted request budget for the calendar minute. 600 units by default, which is roughly two heavy reports a minute or six hundred cheap ones. The budget is per key, not per account, so a runaway agent cannot exhaust the budget your order pipeline depends on. There are no X-RateLimit headers on this path.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 60. Conservative: the fixed minute may roll sooner."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "exhausted": {
                    "value": {
                      "error": {
                        "code": "budget_exhausted",
                        "message": "this key has spent its request budget for the minute"
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The budget could not be verified. This fails closed, unlike the transactional rate limiter, because an unmetered agent in a loop is the more expensive failure and nobody's checkout breaks because a report waited. A Redis outage therefore takes this whole surface down, including whoami and capabilities. No Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "budgetDown": {
                    "value": {
                      "error": {
                        "code": "budget_unavailable",
                        "message": "could not verify the request budget"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/exports": {
      "get": {
        "operationId": "listExports",
        "tags": [
          "management",
          "exports"
        ],
        "servers": [
          {
            "url": "https://api.segmentic.net",
            "description": "Management. Reads and writes audiences, campaigns, reports and exports, and sends one transactional message at a time. Authenticated by a secret key that carries permissions. There is no CORS preflight handler on this mux, so a browser cannot call it."
          }
        ],
        "security": [
          {
            "managementKey": []
          }
        ],
        "summary": "List queued and finished exports",
        "description": "Needs `data.export`. Costs one budget unit. Behind the commercial lock. The only route on this surface that uses the paging envelope, and it never fills it in: `next_cursor` is always absent and `has_more` is always false even when there are more rows. `limit` is read and clamped to 100; `cursor` is read and discarded. `location` is a storage path, not a signed URL. There is no download route on this surface at all: a person collects the file from the panel.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The export jobs. `data` is `[]`, never null.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportListResponse"
                },
                "examples": {
                  "queued": {
                    "value": {
                      "data": [
                        {
                          "id": 42,
                          "kind": "events",
                          "format": "ndjson",
                          "spec": {
                            "segment_id": 12
                          },
                          "status": "queued",
                          "rows_written": 0,
                          "bytes": 0,
                          "attempts": 1,
                          "expires_at": "2026-08-13T09:00:00Z",
                          "requested_by": "api-key:3",
                          "created_at": "2026-08-06T09:00:00Z",
                          "finished_at": null
                        }
                      ],
                      "has_more": false
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential, an unresolvable one, an expired key, a `wk_` write key, or a dashboard session. Four distinct codes: `unauthenticated`, `key_expired`, `write_key_rejected`, `api_key_required`. Sessions are refused outright so this surface has no ambient credential at all, which means there is nothing to forge a request with.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "error": {
                        "code": "unauthenticated",
                        "message": "a valid API key is required"
                      }
                    }
                  },
                  "wrongFamily": {
                    "value": {
                      "error": {
                        "code": "write_key_rejected",
                        "message": "that is an SDK write key (wk_…); this API needs a management key (sk_seg_…)"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Either the key lacks the permission, or the account is locked. The lock triggers at 300 per cent of the plan's allowance or on an issued invoice 75 or more days past due, and `details.reason` says which, so a program can tell 'over the allowance' from 'past due' without matching text. The lock message is Persian. A locked account loses the dashboard, the reports and the exports and keeps everything else: collection and sending go on running. A hole in your data cannot be filled in afterwards and a debt can be collected afterwards, so what is withheld is what can be given back. The verdict is cached for a minute, so settling an invoice takes up to sixty seconds to take effect.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "forbidden": {
                    "value": {
                      "error": {
                        "code": "forbidden",
                        "message": "this key does not carry data.export, see GET /v1/whoami for what it does carry",
                        "need": "data.export"
                      }
                    }
                  },
                  "locked": {
                    "value": {
                      "error": {
                        "code": "account_locked",
                        "message": "مصرف این حساب به سه برابر سهمیه رسیده است",
                        "details": {
                          "reason": "usage_300"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "The key has spent its weighted request budget for the calendar minute. 600 units by default, which is roughly two heavy reports a minute or six hundred cheap ones. The budget is per key, not per account, so a runaway agent cannot exhaust the budget your order pipeline depends on. There are no X-RateLimit headers on this path.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 60. Conservative: the fixed minute may roll sooner."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "exhausted": {
                    "value": {
                      "error": {
                        "code": "budget_exhausted",
                        "message": "this key has spent its request budget for the minute"
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The budget could not be verified. This fails closed, unlike the transactional rate limiter, because an unmetered agent in a loop is the more expensive failure and nobody's checkout breaks because a report waited. A Redis outage therefore takes this whole surface down, including whoami and capabilities. No Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "budgetDown": {
                    "value": {
                      "error": {
                        "code": "budget_unavailable",
                        "message": "could not verify the request budget"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "queueExport",
        "tags": [
          "management",
          "exports"
        ],
        "servers": [
          {
            "url": "https://api.segmentic.net",
            "description": "Management. Reads and writes audiences, campaigns, reports and exports, and sends one transactional message at a time. Authenticated by a secret key that carries permissions. There is no CORS preflight handler on this mux, so a browser cannot call it."
          }
        ],
        "security": [
          {
            "managementKey": []
          }
        ],
        "summary": "Queue an export",
        "description": "Needs `data.export`. Costs 25 budget units. Behind the commercial lock, deliberately: the file this builds is collected through the panel's download route, which is locked, so leaving the queue open would only mean building files a locked account cannot have, at our expense. `format` is coerced, not validated: anything that is not exactly `csv` becomes `ndjson`, including a typo. NDJSON is the default because an export of events with nested properties is not a rectangle, and flattening it into CSV silently loses the nesting. `spec` is passed through untouched and nothing validates what is inside it. Its keys differ per kind and no document in this repository enumerates them, which is a gap you will hit. `requested_by` records the key, as `api-key:<id>`, because 'who exported every customer's address' is a question an audit asks afterwards and the answer has to name something revocable.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExportQueueRequest"
              },
              "examples": {
                "events": {
                  "value": {
                    "kind": "events",
                    "format": "ndjson",
                    "spec": {
                      "segment_id": 12,
                      "from": "2026-07-01",
                      "to": "2026-08-01"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Queued. The file does not exist yet: ninety days of events takes minutes, and a request holding a connection open for minutes dies to a load balancer's timeout. The lifetime is published so a client polling for the file knows how long it has.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportQueueResponse"
                },
                "examples": {
                  "queued": {
                    "value": {
                      "id": 42,
                      "status": "queued",
                      "kind": "events",
                      "expires_after_hours": 168
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "malformed": {
                    "value": {
                      "error": {
                        "code": "malformed_json",
                        "message": "the request body is not valid JSON: unexpected EOF"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential, an unresolvable one, an expired key, a `wk_` write key, or a dashboard session. Four distinct codes: `unauthenticated`, `key_expired`, `write_key_rejected`, `api_key_required`. Sessions are refused outright so this surface has no ambient credential at all, which means there is nothing to forge a request with.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "error": {
                        "code": "unauthenticated",
                        "message": "a valid API key is required"
                      }
                    }
                  },
                  "wrongFamily": {
                    "value": {
                      "error": {
                        "code": "write_key_rejected",
                        "message": "that is an SDK write key (wk_…); this API needs a management key (sk_seg_…)"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Either the key lacks the permission, or the account is locked. The lock triggers at 300 per cent of the plan's allowance or on an issued invoice 75 or more days past due, and `details.reason` says which, so a program can tell 'over the allowance' from 'past due' without matching text. The lock message is Persian. A locked account loses the dashboard, the reports and the exports and keeps everything else: collection and sending go on running. A hole in your data cannot be filled in afterwards and a debt can be collected afterwards, so what is withheld is what can be given back. The verdict is cached for a minute, so settling an invoice takes up to sixty seconds to take effect.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "forbidden": {
                    "value": {
                      "error": {
                        "code": "forbidden",
                        "message": "this key does not carry data.export, see GET /v1/whoami for what it does carry",
                        "need": "data.export"
                      }
                    }
                  },
                  "locked": {
                    "value": {
                      "error": {
                        "code": "account_locked",
                        "message": "مصرف این حساب به سه برابر سهمیه رسیده است",
                        "details": {
                          "reason": "usage_300"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "The kind is not one of the four, and the refusal lists them.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "badKind": {
                    "value": {
                      "error": {
                        "code": "export_kind_invalid",
                        "message": "kind must be one of events, messages, profiles, segment"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "The key has spent its weighted request budget for the calendar minute. 600 units by default, which is roughly two heavy reports a minute or six hundred cheap ones. The budget is per key, not per account, so a runaway agent cannot exhaust the budget your order pipeline depends on. There are no X-RateLimit headers on this path.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 60. Conservative: the fixed minute may roll sooner."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "exhausted": {
                    "value": {
                      "error": {
                        "code": "budget_exhausted",
                        "message": "this key has spent its request budget for the minute"
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The budget could not be verified. This fails closed, unlike the transactional rate limiter, because an unmetered agent in a loop is the more expensive failure and nobody's checkout breaks because a report waited. A Redis outage therefore takes this whole surface down, including whoami and capabilities. No Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "budgetDown": {
                    "value": {
                      "error": {
                        "code": "budget_unavailable",
                        "message": "could not verify the request budget"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/reports/funnel": {
      "post": {
        "operationId": "funnelReport",
        "tags": [
          "management",
          "reports"
        ],
        "servers": [
          {
            "url": "https://api.segmentic.net",
            "description": "Management. Reads and writes audiences, campaigns, reports and exports, and sends one transactional message at a time. Authenticated by a secret key that carries permissions. There is no CORS preflight handler on this mux, so a browser cannot call it."
          }
        ],
        "security": [
          {
            "managementKey": []
          }
        ],
        "summary": "Where people drop out",
        "description": "Needs `analytics.read`. Costs 25 budget units. Behind the commercial lock. Body cap 1 MiB. The deadline is 45 seconds, which is not the `query_timeout_sec` capabilities publishes. `window` is required and must be greater than zero. A checkout funnel measured over thirty days and the same funnel measured over one hour are different questions, and the answer is meaningless without it. Write it as a Go duration with a `d` suffix allowed: `7d`, `1.5d`, `36h`. It must not exceed the range. Between 2 and 12 steps. The range must be under 730 days. Rates come back as fractions, not percentages. Every failure on this route speaks the flat dashboard shape, in Persian. A client that reads only `error.code` will find nothing here.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FunnelRequest"
              },
              "examples": {
                "checkout": {
                  "value": {
                    "steps": [
                      {
                        "name": "product_viewed",
                        "label": "دیدن محصول"
                      },
                      {
                        "name": "checkout_started"
                      },
                      {
                        "name": "order_completed",
                        "filters": [
                          {
                            "prop": "revenue",
                            "op": "gte",
                            "value": "500000"
                          }
                        ]
                      }
                    ],
                    "range": {
                      "from": "2026-05-01T00:00:00Z",
                      "to": "2026-06-01T00:00:00Z"
                    },
                    "window": "7d",
                    "strict": false,
                    "split_by": "city"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The funnel. `buckets` is present only when a breakdown was asked for.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FunnelResult"
                },
                "examples": {
                  "threeSteps": {
                    "value": {
                      "steps": [
                        {
                          "index": 0,
                          "name": "product_viewed",
                          "label": "دیدن محصول",
                          "users": 700,
                          "from_start": 1,
                          "from_previous": 1,
                          "dropped_here": 0
                        },
                        {
                          "index": 1,
                          "name": "checkout_started",
                          "label": "checkout_started",
                          "users": 300,
                          "from_start": 0.4286,
                          "from_previous": 0.4286,
                          "dropped_here": 400
                        }
                      ],
                      "buckets": [
                        {
                          "value": "تهران",
                          "steps": [],
                          "entered": 400,
                          "completed": 180,
                          "conversion": 0.45
                        }
                      ],
                      "entered": 700,
                      "completed": 300,
                      "conversion": 0.4286,
                      "description": "از دیدن محصول تا خرید، در بازهٔ هفت روزه"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The report does not compile. Flat shape with a `code` beside the string, which is a third shape again, and the message is Persian.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FlatCodedError"
                },
                "examples": {
                  "twoSteps": {
                    "value": {
                      "error": "قیف به دست‌کم دو مرحله نیاز دارد",
                      "code": "invalid_report"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential, an unresolvable one, an expired key, a `wk_` write key, or a dashboard session. Four distinct codes: `unauthenticated`, `key_expired`, `write_key_rejected`, `api_key_required`. Sessions are refused outright so this surface has no ambient credential at all, which means there is nothing to forge a request with.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "error": {
                        "code": "unauthenticated",
                        "message": "a valid API key is required"
                      }
                    }
                  },
                  "wrongFamily": {
                    "value": {
                      "error": {
                        "code": "write_key_rejected",
                        "message": "that is an SDK write key (wk_…); this API needs a management key (sk_seg_…)"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Either the key lacks the permission, or the account is locked. The lock triggers at 300 per cent of the plan's allowance or on an issued invoice 75 or more days past due, and `details.reason` says which, so a program can tell 'over the allowance' from 'past due' without matching text. The lock message is Persian. A locked account loses the dashboard, the reports and the exports and keeps everything else: collection and sending go on running. A hole in your data cannot be filled in afterwards and a debt can be collected afterwards, so what is withheld is what can be given back. The verdict is cached for a minute, so settling an invoice takes up to sixty seconds to take effect.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "forbidden": {
                    "value": {
                      "error": {
                        "code": "forbidden",
                        "message": "this key does not carry analytics.read, see GET /v1/whoami for what it does carry",
                        "need": "analytics.read"
                      }
                    }
                  },
                  "locked": {
                    "value": {
                      "error": {
                        "code": "account_locked",
                        "message": "مصرف این حساب به سه برابر سهمیه رسیده است",
                        "details": {
                          "reason": "usage_300"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "The key has spent its weighted request budget for the calendar minute. 600 units by default, which is roughly two heavy reports a minute or six hundred cheap ones. The budget is per key, not per account, so a runaway agent cannot exhaust the budget your order pipeline depends on. There are no X-RateLimit headers on this path.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 60. Conservative: the fixed minute may roll sooner."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "exhausted": {
                    "value": {
                      "error": {
                        "code": "budget_exhausted",
                        "message": "this key has spent its request budget for the minute"
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The budget could not be verified. This fails closed, unlike the transactional rate limiter, because an unmetered agent in a loop is the more expensive failure and nobody's checkout breaks because a report waited. A Redis outage therefore takes this whole surface down, including whoami and capabilities. No Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "budgetDown": {
                    "value": {
                      "error": {
                        "code": "budget_unavailable",
                        "message": "could not verify the request budget"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/reports/retention": {
      "post": {
        "operationId": "retentionReport",
        "tags": [
          "management",
          "reports"
        ],
        "servers": [
          {
            "url": "https://api.segmentic.net",
            "description": "Management. Reads and writes audiences, campaigns, reports and exports, and sends one transactional message at a time. Authenticated by a secret key that carries permissions. There is no CORS preflight handler on this mux, so a browser cannot call it."
          }
        ],
        "security": [
          {
            "managementKey": []
          }
        ],
        "summary": "How much of each cohort came back",
        "description": "Needs `analytics.read`. Costs 25 budget units. Behind the commercial lock. Body cap 1 MiB, deadline 45 seconds. `start` and `return` are separate because 'came back' rarely means 'did the same thing again'. A shopping app cares who signed up and then bought; asking whether they opened the app again flatters the number and answers nothing. An empty `name` on either means any activity. `periods` defaults to 30 and is capped at 60. Cohort boundaries are computed in Tehran on the Iranian calendar, because the warehouse's own month function is Gregorian and its week cannot start on Saturday. `observable: false` means the report has not run long enough to know. A cohort that started yesterday has no day-30 number, and rendering that as zero is how a healthy product looks like it is dying. The average curve is weighted, total returners over total starters, and counts only cells that could have been observed.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RetentionRequest"
              },
              "examples": {
                "signupToPurchase": {
                  "value": {
                    "start": {
                      "name": "signed_up"
                    },
                    "return": {
                      "name": "order_completed"
                    },
                    "range": {
                      "from": "2026-01-01T00:00:00Z",
                      "to": "2026-07-01T00:00:00Z"
                    },
                    "granularity": "week",
                    "periods": 12
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The cohort grid and the weighted curve under it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RetentionResult"
                },
                "examples": {
                  "weekly": {
                    "value": {
                      "granularity": "week",
                      "period_label": "هفته",
                      "cohorts": [
                        {
                          "cohort": "1405-02-11",
                          "label": "هفتهٔ ۱۱ اردیبهشت",
                          "size": 4021,
                          "cells": [
                            {
                              "period": 0,
                              "users": 4021,
                              "rate": 1,
                              "observable": true
                            },
                            {
                              "period": 1,
                              "users": 1802,
                              "rate": 0.448,
                              "observable": true
                            },
                            {
                              "period": 8,
                              "users": 0,
                              "rate": 0,
                              "observable": false
                            }
                          ]
                        }
                      ],
                      "average": [
                        {
                          "period": 0,
                          "users": 0,
                          "rate": 1,
                          "observable": true
                        }
                      ],
                      "description": "از ثبت‌نام تا خرید، هفتگی"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The report does not compile. Flat shape with a code, Persian.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FlatCodedError"
                },
                "examples": {
                  "badGranularity": {
                    "value": {
                      "error": "بازهٔ زمانی نامعتبر است",
                      "code": "invalid_report"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential, an unresolvable one, an expired key, a `wk_` write key, or a dashboard session. Four distinct codes: `unauthenticated`, `key_expired`, `write_key_rejected`, `api_key_required`. Sessions are refused outright so this surface has no ambient credential at all, which means there is nothing to forge a request with.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "error": {
                        "code": "unauthenticated",
                        "message": "a valid API key is required"
                      }
                    }
                  },
                  "wrongFamily": {
                    "value": {
                      "error": {
                        "code": "write_key_rejected",
                        "message": "that is an SDK write key (wk_…); this API needs a management key (sk_seg_…)"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Either the key lacks the permission, or the account is locked. The lock triggers at 300 per cent of the plan's allowance or on an issued invoice 75 or more days past due, and `details.reason` says which, so a program can tell 'over the allowance' from 'past due' without matching text. The lock message is Persian. A locked account loses the dashboard, the reports and the exports and keeps everything else: collection and sending go on running. A hole in your data cannot be filled in afterwards and a debt can be collected afterwards, so what is withheld is what can be given back. The verdict is cached for a minute, so settling an invoice takes up to sixty seconds to take effect.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "forbidden": {
                    "value": {
                      "error": {
                        "code": "forbidden",
                        "message": "this key does not carry analytics.read, see GET /v1/whoami for what it does carry",
                        "need": "analytics.read"
                      }
                    }
                  },
                  "locked": {
                    "value": {
                      "error": {
                        "code": "account_locked",
                        "message": "مصرف این حساب به سه برابر سهمیه رسیده است",
                        "details": {
                          "reason": "usage_300"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "The key has spent its weighted request budget for the calendar minute. 600 units by default, which is roughly two heavy reports a minute or six hundred cheap ones. The budget is per key, not per account, so a runaway agent cannot exhaust the budget your order pipeline depends on. There are no X-RateLimit headers on this path.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 60. Conservative: the fixed minute may roll sooner."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "exhausted": {
                    "value": {
                      "error": {
                        "code": "budget_exhausted",
                        "message": "this key has spent its request budget for the minute"
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The budget could not be verified. This fails closed, unlike the transactional rate limiter, because an unmetered agent in a loop is the more expensive failure and nobody's checkout breaks because a report waited. A Redis outage therefore takes this whole surface down, including whoami and capabilities. No Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "budgetDown": {
                    "value": {
                      "error": {
                        "code": "budget_unavailable",
                        "message": "could not verify the request budget"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/messages": {
      "post": {
        "operationId": "sendTransactionalMessage",
        "tags": [
          "management",
          "messaging"
        ],
        "servers": [
          {
            "url": "https://api.segmentic.net",
            "description": "Management. Reads and writes audiences, campaigns, reports and exports, and sends one transactional message at a time. Authenticated by a secret key that carries permissions. There is no CORS preflight handler on this mux, so a browser cannot call it."
          }
        ],
        "security": [
          {
            "managementKey": []
          }
        ],
        "summary": "Send one message to one person, now",
        "description": "Needs `campaign.send`. Costs one budget unit, which is trivial in query terms and enormous in consequence. Body cap 256 KiB. The only route on this surface that refuses unknown fields. A caller who mistyped `idempotency_key` would otherwise get a brand new key on every retry and send a message per attempt. `category` may be `transactional` or `critical`. `marketing` is refused, and the refusal is load-bearing: this endpoint bypasses frequency caps and quiet hours, so accepting marketing here would hand every customer a documented way around their own sending rules, and the first time it mattered would be a 3am promotional SMS to a whole list. It does not bypass an operator's suppression, and it never bypasses a channel the recipient switched off unless the category is `critical`. `channel` here is compared against the literal list and not put through the alias table, so `web` is refused and `webpush` is what you send. `messenger` and `webhook` are campaign-only and refused. A 200 does not mean delivered. Read `reason` and `reason_fa`: they are set when the message was deliberately not sent. This route is metered twice, by the per-key request budget and by a separate per-account request limiter, and the two refuse with different bodies. The second one is off by default and fails open.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9._:-]{8,200}$"
            },
            "description": "An alternative to the body field, because it is what most HTTP clients already have a retry wrapper for. The body field wins when both are present."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TransactionalRequest"
              },
              "examples": {
                "loginCode": {
                  "value": {
                    "user_id": "u_9137",
                    "channel": "sms",
                    "category": "transactional",
                    "template_id": 42,
                    "vars": {
                      "code": "8391"
                    },
                    "idempotency_key": "order-8821-shipped"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The send happened, or it happened earlier and this is the stored answer verbatim. `replayed` tells the two apart, which matters when the first attempt timed out and you do not know which happened. Carries X-RateLimit-Limit and X-RateLimit-Remaining when the per-account limiter is configured, on success as well as refusal. Those describe that limiter, not the request budget, which has no headers at all. A 200 is also the answer when the message went out and the ledger write failed afterwards: the message really did go, and a caller told otherwise would retry and send a second one.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TransactionalResult"
                },
                "examples": {
                  "sent": {
                    "value": {
                      "message_id": "t7.order-8821-shipped",
                      "status": "sent",
                      "sent_at": "2026-08-06T09:12:41Z"
                    }
                  },
                  "replayed": {
                    "value": {
                      "message_id": "t7.order-8821-shipped",
                      "status": "sent",
                      "replayed": true,
                      "sent_at": "2026-08-06T09:12:41Z"
                    }
                  },
                  "withheld": {
                    "value": {
                      "message_id": "t7.order-8822-shipped",
                      "status": "withheld",
                      "reason": "opted_out",
                      "reason_fa": "این کاربر لغو اشتراک کرده است",
                      "sent_at": "2026-08-06T09:13:02Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, an unknown field, or a caller error. All flat, with no code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FlatError"
                },
                "examples": {
                  "noUser": {
                    "value": {
                      "error": "transactional: user_id is required"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential, an unresolvable one, an expired key, a `wk_` write key, or a dashboard session. Four distinct codes: `unauthenticated`, `key_expired`, `write_key_rejected`, `api_key_required`. Sessions are refused outright so this surface has no ambient credential at all, which means there is nothing to forge a request with.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "error": {
                        "code": "unauthenticated",
                        "message": "a valid API key is required"
                      }
                    }
                  },
                  "wrongFamily": {
                    "value": {
                      "error": {
                        "code": "write_key_rejected",
                        "message": "that is an SDK write key (wk_…); this API needs a management key (sk_seg_…)"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key's role, intersected with its scopes, does not carry the permission. `need` names it exactly. Do not retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "forbidden": {
                    "value": {
                      "error": {
                        "code": "forbidden",
                        "message": "this key does not carry campaign.send, see GET /v1/whoami for what it does carry",
                        "need": "campaign.send"
                      }
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The same key is already in flight. Carries Retry-After: 1. Note that a reservation abandoned by a crashed process is never swept, so a key stuck here stays stuck until somebody clears the row by hand.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FlatError"
                },
                "examples": {
                  "inFlight": {
                    "value": {
                      "error": "transactional: a message with this idempotency key is already in flight"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "The key has spent its weighted request budget for the calendar minute. 600 units by default, which is roughly two heavy reports a minute or six hundred cheap ones. The budget is per key, not per account, so a runaway agent cannot exhaust the budget your order pipeline depends on. There are no X-RateLimit headers on this path.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Always 60. Conservative: the fixed minute may roll sooner."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "exhausted": {
                    "value": {
                      "error": {
                        "code": "budget_exhausted",
                        "message": "this key has spent its request budget for the minute"
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The budget could not be verified. This fails closed, unlike the transactional rate limiter, because an unmetered agent in a loop is the more expensive failure and nobody's checkout breaks because a report waited. A Redis outage therefore takes this whole surface down, including whoami and capabilities. No Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "examples": {
                  "budgetDown": {
                    "value": {
                      "error": {
                        "code": "budget_unavailable",
                        "message": "could not verify the request budget"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "writeKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "The write key: `wk_seg_` followed by 43 base64url characters. Public by design, because it ships inside your own bundle. It carries an account and an app and nothing else, and it reads nothing. The collector reads it from three places, in this order: `Authorization: Bearer <key>`, the header `X-Segmentic-Key`, and the query parameter `write_key`. The last exists because an image beacon and navigator.sendBeacon cannot set a header. Only the literal `Bearer ` prefix is stripped from Authorization; any other scheme falls through to the other two places. The prefix is chosen so a leak can be triaged in one glance: a `wk_` in a public bundle is working as designed, an `sk_` in the same place is an incident. Only a SHA-256 hash is stored, so a database dump does not hand anyone the ability to forge your events. Revoking one takes effect within a minute, because the lookup is cached that long."
      },
      "managementKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "The management key: `sk_seg_` followed by 43 base64url characters. Secret. Never put one in browser code or inside a mobile app. `Authorization: Bearer <key>` is the form to use. `X-Segmentic-Key` is also read, and a session cookie is read last and then refused, because a session is an ambient credential a browser attaches automatically and every CSRF question follows from accepting one. A key carries a role, and the role decides what it may do. A key can never be an owner, so no key holds tenant.transfer or tenant.delete. Keys expire after 365 days by default, and an expired one answers 401 `key_expired` rather than a generic refusal. The plaintext is shown once at creation and never again."
      }
    },
    "schemas": {
      "StatusResponse": {
        "type": "object",
        "required": [
          "status",
          "service",
          "version"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "service": {
            "type": "string",
            "enum": [
              "collector",
              "api"
            ],
            "description": "Which of the two hosts answered."
          },
          "version": {
            "type": "string",
            "description": "The build stamp. `dev` on an unstamped build."
          }
        }
      },
      "Warning": {
        "type": "object",
        "description": "Something we accepted and altered. A warning never changes the status code: the event was stored.",
        "required": [
          "code"
        ],
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "generated_message_id",
              "timestamp_in_future",
              "timestamp_too_old",
              "too_many_properties",
              "too_many_traits",
              "unserialisable_property",
              "invalid_phone",
              "invalid_national_id"
            ]
          },
          "field": {
            "type": "string",
            "description": "Which field it is about."
          },
          "note": {
            "type": "string",
            "description": "A sentence explaining what was done. Not every warning carries one."
          }
        }
      },
      "ItemError": {
        "type": "object",
        "description": "One rejected item of a batch. `index` is into your own array, so retry logic can find the item without matching on content.",
        "required": [
          "index",
          "reason"
        ],
        "properties": {
          "index": {
            "type": "integer"
          },
          "reason": {
            "type": "string",
            "description": "The stable sentinel, sometimes followed by ': ' and the value you sent. Match on the prefix. One of unknown_type, missing_identity, missing_event_name, event_name_too_long, event_name_invalid_chars, id_too_long, missing_previous_id."
          }
        }
      },
      "IngestResponse": {
        "type": "object",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "accepted": {
            "type": "integer",
            "description": "Omitted when zero, so a wholly rejected batch has no `accepted` key at all. Duplicates are counted here: the SDK already delivered them once and must stop retrying."
          },
          "rejected": {
            "type": "integer",
            "description": "Omitted when zero."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Warning"
            },
            "description": "Capped at roughly 50 entries on a batch. Metrics count all of them."
          },
          "errors": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ItemError"
            }
          }
        }
      },
      "IngestError": {
        "type": "object",
        "description": "The ingest error shape. Flat, and there is no `code` field anywhere on this surface: the machine-readable values are the HTTP status and, for per-item failures, the leading sentinel in `errors[].reason`.",
        "required": [
          "status",
          "message"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "error"
            ]
          },
          "message": {
            "type": "string",
            "description": "A sentence. English, except the quota refusal, which is Persian."
          }
        }
      },
      "Envelope": {
        "type": "object",
        "description": "One message. The wire format and the internal format are deliberately separate types, because customers' SDKs will send imperfect data for ever. IP, User-Agent and the browser and bot fields derived from them are taken from the connection and never from the body: a client must not be able to fake its own geo or device.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "track",
              "identify",
              "page",
              "screen",
              "alias"
            ],
            "description": "Required on each item of /v1/batch and on each item of the management API's /v1/events. Ignored on the five single-event routes, where the path decides."
          },
          "message_id": {
            "type": "string",
            "description": "What makes a retry safe. SDKs on flaky mobile networks resend aggressively, so without it a purchase count silently doubles. One is generated when absent and a warning is attached: the SDK is at fault and somebody should fix it. Maximum 256 bytes. De-duplication is per account, over a 48-hour window."
          },
          "event": {
            "type": "string",
            "description": "The event name. Required for `track`. Maximum 128 bytes after normalisation."
          },
          "user_id": {
            "type": "string",
            "description": "Maximum 256 bytes. One of `user_id` or `anonymous_id` is required."
          },
          "anonymous_id": {
            "type": "string",
            "description": "Maximum 256 bytes. Persisting it is the SDK's job; the server sets no cookie."
          },
          "previous_id": {
            "type": "string",
            "description": "Required for `alias`, ignored everywhere else."
          },
          "timestamp": {
            "type": "string",
            "format": "date-time",
            "description": "RFC 3339 only. Epoch seconds and bare dates fail to parse and the whole request is a 400. Defaults to the receive time. More than an hour ahead is clamped to now; older than your account's window is clamped to the edge of it. Both clamps warn and neither refuses."
          },
          "sent_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the SDK put it on the wire. Present, it enables clock-skew correction: a device two hours slow has its timestamp moved forward by the measured skew, silently, and only when the result still falls inside the window."
          },
          "properties": {
            "type": "object",
            "additionalProperties": true,
            "description": "At most 256 keys are kept, and which 256 survive is not deterministic. Keys are normalised: whitespace, dots and dashes become underscores, control characters are dropped, 128 bytes maximum. String values are truncated at 8192 bytes. A null value is dropped entirely rather than stored as an empty string, because a null means 'not set' and storing it would make an is-not-set filter wrong. Revenue is read from `revenue`, then `total`, then `value`, then `price` times `quantity`; `currency` defaults to IRR and is never converted."
          },
          "traits": {
            "type": "object",
            "additionalProperties": true,
            "description": "At most 256. `email` is lower-cased. `phone` is parsed to E.164 and writes `phone_operator` beside it; an unparseable one is kept verbatim with a warning. `national_id` is checked and dropped if the check digit fails, which is the opposite of what `phone` does. `gender` folds to male, female or other. Numbers and booleans are written to a numeric map as well as a text one, which is what makes a range filter work; a numeric-looking string is never parsed, because that would throw away the leading zero of a postcode."
          },
          "context": {
            "$ref": "#/components/schemas/Context"
          }
        }
      },
      "Batch": {
        "type": "object",
        "required": [
          "batch"
        ],
        "properties": {
          "batch": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Envelope"
            },
            "description": "Between 1 and 500 items, each carrying its own `type`."
          },
          "sent_at": {
            "type": "string",
            "format": "date-time",
            "description": "Filled in on items that have none of their own. A per-item value wins."
          },
          "context": {
            "$ref": "#/components/schemas/Context",
            "description": "Filled in on items that have none of their own. A per-item value wins."
          }
        }
      },
      "Context": {
        "type": "object",
        "description": "What the SDK collects once and attaches to every message. Three of these are accepted on the wire and stored nowhere: `screen` entirely, the latitude and longitude inside `location`, and everything in `device` except `manufacturer`, `model`, `type` and `push_provider`. A push token in the event stream would be copied into the warehouse, every export and every backup, for a value the device registry already owns.",
        "properties": {
          "app": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string",
                "description": "Stored as `app_version`. 64 bytes."
              },
              "build": {
                "type": "string"
              },
              "namespace": {
                "type": "string"
              }
            }
          },
          "device": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "description": "Accepted and not stored."
              },
              "manufacturer": {
                "type": "string",
                "description": "Stored as `device_vendor`. 64 bytes."
              },
              "model": {
                "type": "string",
                "description": "128 bytes."
              },
              "type": {
                "type": "string",
                "description": "android, ios or web. 32 bytes."
              },
              "name": {
                "type": "string",
                "description": "Accepted and not stored."
              },
              "push_provider": {
                "type": "string",
                "description": "fcm, bazaar, myket, mqtt or apns. 16 bytes."
              },
              "push_token": {
                "type": "string",
                "description": "Accepted and deliberately not stored on the event."
              },
              "has_gms": {
                "type": "boolean",
                "description": "Accepted and not stored on the event."
              },
              "ad_tracking_enabled": {
                "type": "boolean",
                "description": "Accepted and not stored on the event."
              }
            }
          },
          "os": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string",
                "description": "Lower-cased. 32 bytes."
              },
              "version": {
                "type": "string",
                "description": "32 bytes."
              }
            }
          },
          "screen": {
            "type": "object",
            "description": "Accepted on the wire and copied nowhere.",
            "properties": {
              "width": {
                "type": "integer"
              },
              "height": {
                "type": "integer"
              },
              "density": {
                "type": "number"
              }
            }
          },
          "network": {
            "type": "object",
            "properties": {
              "carrier": {
                "type": "string",
                "description": "Stored. 64 bytes."
              },
              "cellular": {
                "type": "boolean",
                "description": "Accepted and not stored."
              },
              "wifi": {
                "type": "boolean",
                "description": "Accepted and not stored."
              }
            }
          },
          "page": {
            "type": "object",
            "properties": {
              "url": {
                "type": "string",
                "description": "2048 bytes."
              },
              "path": {
                "type": "string",
                "description": "2048 bytes."
              },
              "title": {
                "type": "string",
                "description": "512 bytes, Persian-normalised."
              },
              "referrer": {
                "type": "string",
                "description": "2048 bytes."
              },
              "search": {
                "type": "string",
                "description": "Accepted and not stored."
              }
            }
          },
          "campaign": {
            "type": "object",
            "description": "How an event is tied back to the message that caused it.",
            "properties": {
              "source": {
                "type": "string",
                "description": "Stored as `utm_source`. 128 bytes."
              },
              "medium": {
                "type": "string",
                "description": "Stored as `utm_medium`. 128 bytes."
              },
              "name": {
                "type": "string",
                "description": "Stored as `utm_campaign`. 128 bytes."
              },
              "term": {
                "type": "string",
                "description": "Stored as `utm_term`. 128 bytes."
              },
              "content": {
                "type": "string",
                "description": "Stored as `utm_content`. 128 bytes."
              },
              "message_id": {
                "type": "string",
                "description": "Stored as `source_message_id`. 256 bytes."
              },
              "campaign_id": {
                "type": "integer",
                "format": "int64"
              },
              "journey_id": {
                "type": "integer",
                "format": "int64"
              },
              "variant_id": {
                "type": "string",
                "description": "64 bytes."
              },
              "token": {
                "type": "string",
                "description": "The signature that came back with the link, 128 bytes. Kept on its own field and out of the property map on purpose: the user timeline returns properties verbatim, and a token visible there would be a token anyone with a dashboard key could replay."
              }
            }
          },
          "library": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string",
                "description": "segmentic-js, segmentic-android or segmentic-ios. 64 bytes."
              },
              "version": {
                "type": "string",
                "description": "32 bytes."
              }
            }
          },
          "location": {
            "type": "object",
            "description": "The only source of country, region and city on the deployed build: IP geolocation is not configured, so an event with no `context.location` has no geography at all.",
            "properties": {
              "country": {
                "type": "string",
                "description": "64 bytes."
              },
              "region": {
                "type": "string",
                "description": "64 bytes."
              },
              "city": {
                "type": "string",
                "description": "64 bytes, Persian-normalised."
              },
              "latitude": {
                "type": "number",
                "description": "Accepted and dropped."
              },
              "longitude": {
                "type": "number",
                "description": "Accepted and dropped."
              }
            }
          },
          "locale": {
            "type": "string",
            "description": "For example fa-IR. 32 bytes."
          },
          "timezone": {
            "type": "string",
            "description": "For example Asia/Tehran. 64 bytes."
          },
          "session_id": {
            "type": "string",
            "description": "256 bytes. The web SDK mints a new one after 30 idle minutes."
          },
          "ip": {
            "type": "string",
            "description": "Used only when the server has no address for the connection."
          },
          "user_agent": {
            "type": "string",
            "description": "Ignored. The header wins, always."
          }
        }
      },
      "DeviceWarning": {
        "type": "object",
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "empty_token",
              "token_too_long",
              "transport_not_supported",
              "fcm_without_gms"
            ]
          },
          "message": {
            "type": "string"
          },
          "field": {
            "type": "string"
          }
        }
      },
      "DeviceResponse": {
        "type": "object",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "error"
            ]
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DeviceWarning"
            }
          },
          "message": {
            "type": "string",
            "description": "Present on a failure."
          }
        }
      },
      "DeviceRegistration": {
        "type": "object",
        "required": [
          "device_id",
          "platform"
        ],
        "properties": {
          "device_id": {
            "type": "string",
            "description": "Required. Maximum 256 bytes."
          },
          "user_id": {
            "type": "string",
            "description": "One of `user_id` or `anonymous_id` is required."
          },
          "anonymous_id": {
            "type": "string"
          },
          "platform": {
            "type": "string",
            "description": "android, ios, web, windows, macos or linux. Aliases are accepted: mac, mac os, macos, osx, darwin, win, win32, win64, browser, webapp, iphone, ipad, ios. `server` is refused."
          },
          "tokens": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Transport name to token. One device can hold several routes at once and which one delivers is decided at send time. Transports: fcm, bazaar, myket, apns, mqtt, webpush. Android accepts fcm, bazaar, myket and mqtt; iOS accepts apns and mqtt; web, windows, macos and linux accept webpush. A token on any other transport is kept out and warned about. APNs tokens of the older '<a1b2 c3d4>' shape are repaired rather than rejected, because sending that verbatim is refused by Apple for every message for ever."
          },
          "push_provider": {
            "type": "string",
            "description": "The older single-route form. `tokens` wins when both are present."
          },
          "push_token": {
            "type": "string",
            "description": "The older single-route form."
          },
          "has_gms": {
            "type": "boolean",
            "description": "Whether Google Play Services looked usable. Absent means the SDK did not say, which is not the same as false and is not stored as false."
          },
          "push_enabled": {
            "type": "boolean",
            "description": "The operating system's notification permission. Absent is treated as enabled, so an old SDK does not mute its own users. False with no token is accepted, because it is a real state change."
          },
          "app_version": {
            "type": "string"
          },
          "manufacturer": {
            "type": "string"
          },
          "model": {
            "type": "string"
          },
          "os_name": {
            "type": "string"
          },
          "os_version": {
            "type": "string"
          },
          "locale": {
            "type": "string"
          },
          "timezone": {
            "type": "string"
          },
          "sdk_name": {
            "type": "string"
          },
          "sdk_version": {
            "type": "string"
          }
        }
      },
      "DeviceUnregisterRequest": {
        "type": "object",
        "required": [
          "device_id"
        ],
        "properties": {
          "device_id": {
            "type": "string"
          },
          "user_id": {
            "type": "string"
          },
          "revoked": {
            "type": "boolean",
            "description": "False, the default, is a sign-out: the user is detached and the token stays. True marks the install gone."
          }
        }
      },
      "WebPushSubscription": {
        "type": "object",
        "required": [
          "endpoint",
          "p256dh",
          "auth"
        ],
        "properties": {
          "endpoint": {
            "type": "string",
            "description": "The push service's URL for this browser."
          },
          "p256dh": {
            "type": "string",
            "description": "The browser's public key, base64url, uncompressed P-256 point."
          },
          "auth": {
            "type": "string",
            "description": "A 16-byte shared secret the browser generated."
          }
        }
      },
      "WebPushSubscribeRequest": {
        "type": "object",
        "required": [
          "user_id"
        ],
        "properties": {
          "user_id": {
            "type": "string"
          },
          "subscription": {
            "$ref": "#/components/schemas/WebPushSubscription"
          },
          "endpoint": {
            "type": "string",
            "description": "The flattened form."
          },
          "p256dh": {
            "type": "string",
            "description": "The flattened form."
          },
          "auth": {
            "type": "string",
            "description": "The flattened form."
          }
        }
      },
      "WebPushUnsubscribeRequest": {
        "type": "object",
        "required": [
          "endpoint"
        ],
        "properties": {
          "endpoint": {
            "type": "string"
          }
        }
      },
      "MessengerLinkRequest": {
        "type": "object",
        "required": [
          "user_id",
          "platform",
          "chat_id"
        ],
        "properties": {
          "user_id": {
            "type": "string"
          },
          "platform": {
            "type": "string",
            "enum": [
              "bale",
              "eitaa",
              "rubika"
            ]
          },
          "chat_id": {
            "type": "string"
          },
          "username": {
            "type": "string"
          },
          "source": {
            "type": "string",
            "description": "How the chat id was obtained. `bot_start` is the one that carries consent."
          }
        }
      },
      "MessengerUnlinkRequest": {
        "type": "object",
        "required": [
          "user_id",
          "platform"
        ],
        "properties": {
          "user_id": {
            "type": "string"
          },
          "platform": {
            "type": "string",
            "enum": [
              "bale",
              "eitaa",
              "rubika"
            ]
          }
        }
      },
      "InboxRequest": {
        "type": "object",
        "required": [
          "user_id",
          "user_hash"
        ],
        "properties": {
          "user_id": {
            "type": "string"
          },
          "user_hash": {
            "type": "string",
            "description": "Lower-case hex HMAC-SHA256(your identity secret, user_id), computed by your backend at sign-in."
          },
          "limit": {
            "type": "integer",
            "description": "Zero leaves the bound to the store."
          },
          "seen": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Message ids to mark seen."
          },
          "dismissed": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "InboxEntry": {
        "type": "object",
        "required": [
          "message_id",
          "title",
          "body",
          "surface",
          "created_at",
          "expires_at",
          "seen"
        ],
        "properties": {
          "message_id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "body": {
            "type": "string"
          },
          "image": {
            "type": "string"
          },
          "deeplink": {
            "type": "string"
          },
          "surface": {
            "type": "string"
          },
          "token": {
            "type": "string",
            "description": "The attribution signature. Send it back on the `message_opened` event so the open can be proven to belong to a message we really sent."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "seen": {
            "type": "boolean"
          }
        }
      },
      "InboxResponse": {
        "type": "object",
        "required": [
          "status",
          "messages"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "error"
            ]
          },
          "messages": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InboxEntry"
            }
          },
          "message": {
            "type": "string",
            "description": "Present on a failure."
          }
        }
      },
      "OnsiteFetchResponse": {
        "type": "object",
        "required": [
          "campaigns",
          "cache_seconds"
        ],
        "properties": {
          "campaigns": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OnsiteCampaign"
            }
          },
          "cache_seconds": {
            "type": "integer",
            "description": "Always 60. Echoed so the browser applies the same cap arithmetic the server does, from local storage, without asking."
          }
        }
      },
      "OnsiteCampaign": {
        "type": "object",
        "description": "The account id is never serialised.",
        "required": [
          "id",
          "name",
          "kind",
          "status",
          "content",
          "targeting"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "name": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "description": "banner, modal or survey."
          },
          "status": {
            "type": "string"
          },
          "content": {
            "type": "object",
            "properties": {
              "headline": {
                "type": "string"
              },
              "body": {
                "type": "string"
              },
              "image_url": {
                "type": "string"
              },
              "button_text": {
                "type": "string"
              },
              "button_url": {
                "type": "string"
              },
              "position": {
                "type": "string"
              },
              "background": {
                "type": "string"
              },
              "text_color": {
                "type": "string"
              },
              "accent": {
                "type": "string"
              },
              "question": {
                "type": "string"
              },
              "nps": {
                "type": "boolean"
              },
              "follow_up": {
                "type": "string"
              },
              "choices": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "thank_you": {
                "type": "string"
              }
            }
          },
          "targeting": {
            "type": "object",
            "properties": {
              "url_contains": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "url_not_contains": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "devices": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "delay_seconds": {
                "type": "integer"
              },
              "scroll_percent": {
                "type": "integer"
              },
              "on_exit_intent": {
                "type": "boolean"
              },
              "new_visitors_only": {
                "type": "boolean"
              },
              "returning_only": {
                "type": "boolean"
              },
              "logged_in": {
                "type": "boolean"
              },
              "traits": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                }
              }
            }
          },
          "max_impressions": {
            "type": "integer"
          },
          "cooldown_hours": {
            "type": "integer"
          },
          "dismissible": {
            "type": "boolean"
          },
          "starts_at": {
            "type": "string",
            "format": "date-time"
          },
          "ends_at": {
            "type": "string",
            "format": "date-time"
          },
          "impressions": {
            "type": "integer",
            "format": "int64"
          },
          "clicks": {
            "type": "integer",
            "format": "int64"
          },
          "dismissals": {
            "type": "integer",
            "format": "int64"
          }
        }
      },
      "OnsiteEventRequest": {
        "type": "object",
        "required": [
          "campaign_id"
        ],
        "properties": {
          "campaign_id": {
            "type": "integer",
            "format": "int64"
          },
          "user_id": {
            "type": "string",
            "description": "One of `user_id` or `anonymous_id` is required."
          },
          "anonymous_id": {
            "type": "string"
          },
          "action": {
            "type": "string",
            "description": "impression (also the empty string), click, dismiss or convert. Case-insensitive. Not read by /v1/onsite/response."
          },
          "page_url": {
            "type": "string"
          },
          "score": {
            "type": "integer",
            "description": "/v1/onsite/response only. 0 to 10 when the campaign is NPS, and forced to -1, meaning no score, when it is not. Omitting it on an NPS campaign is a 400: a missing score is not a zero, and zero is the worst answer on the scale."
          },
          "answers": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "/v1/onsite/response only. Where a multiple-choice or free-text answer belongs; any key outside this map is discarded when the body is decoded. Free text over 2000 characters is truncated rather than refused."
          }
        }
      },
      "WebhookResponse": {
        "type": "object",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "accepted": {
            "type": "integer",
            "description": "Events produced. Zero for a topic we do not map."
          }
        }
      },
      "BounceOutcome": {
        "type": "object",
        "required": [
          "reports",
          "recorded",
          "suppressed",
          "opted_out",
          "rejected"
        ],
        "properties": {
          "reports": {
            "type": "integer"
          },
          "recorded": {
            "type": "integer"
          },
          "suppressed": {
            "type": "integer"
          },
          "opted_out": {
            "type": "integer"
          },
          "rejected": {
            "type": "integer"
          }
        }
      },
      "PublicError": {
        "type": "object",
        "description": "The management surface's error shape. Nested under `error`, unlike the ingest one. `code` is the contract; the message is not, and an integration that branches on message text will break the first time the wording is improved. Parse defensively. Eleven routes reuse handlers from the dashboard and answer with a flat string instead, so `error` may be a string or an object: branch on its type before reading `code`.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable and machine-readable."
              },
              "message": {
                "type": "string",
                "description": "A sentence. English, except the lock and quota refusals, which are Persian."
              },
              "details": {
                "description": "Per-field problems, so you can point at the offending part of your own payload. An object on most refusals, an array on `all_events_rejected`."
              },
              "need": {
                "type": "string",
                "description": "The missing permission. Present on a 403 `forbidden` only."
              }
            }
          }
        }
      },
      "FlatError": {
        "type": "object",
        "description": "The other error shape on this surface, produced by the handlers shared with the dashboard. There is no code. It appears on the two schema routes, both audience routes, both list routes, both by-id reads, both report routes and every failure of POST /v1/messages.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          }
        }
      },
      "FlatCodedError": {
        "type": "object",
        "description": "A third shape, on the two report routes only: flat, with `code` as a sibling of the string rather than inside an object.",
        "required": [
          "error",
          "code"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Persian."
          },
          "code": {
            "type": "string",
            "enum": [
              "invalid_report"
            ]
          }
        }
      },
      "Whoami": {
        "type": "object",
        "required": [
          "tenant_id",
          "api_key_id",
          "role",
          "permissions",
          "scoped"
        ],
        "properties": {
          "tenant_id": {
            "type": "integer",
            "description": "Your account's numeric id."
          },
          "api_key_id": {
            "type": "integer",
            "description": "Note the wire name: not `key_id`."
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "admin",
              "marketer",
              "analyst",
              "viewer",
              "approver",
              "finance"
            ],
            "description": "A key can never be an owner, and therefore never holds tenant.transfer or tenant.delete."
          },
          "permissions": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Alphabetically sorted, and `[]` rather than null when empty."
          },
          "scoped": {
            "type": "boolean",
            "description": "Whether the key was narrowed below its role. False on every key the product can create: scopes are readable from the database and nothing writes them."
          }
        }
      },
      "Capabilities": {
        "type": "object",
        "required": [
          "version",
          "features",
          "limits"
        ],
        "properties": {
          "version": {
            "type": "string",
            "enum": [
              "v1"
            ]
          },
          "features": {
            "type": "object",
            "additionalProperties": {
              "type": "boolean"
            }
          },
          "limits": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          }
        }
      },
      "EventSchemaResponse": {
        "type": "object",
        "required": [
          "events"
        ],
        "properties": {
          "events": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "name",
                "volume"
              ],
              "properties": {
                "name": {
                  "type": "string"
                },
                "volume": {
                  "type": "integer",
                  "format": "int64",
                  "description": "Over a ninety-day window."
                },
                "prop_keys": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "last_seen": {
                  "type": "string",
                  "description": "YYYY-MM-DD."
                }
              }
            }
          }
        }
      },
      "TraitSchemaResponse": {
        "type": "object",
        "required": [
          "traits",
          "schema"
        ],
        "properties": {
          "traits": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Names only, kept for compatibility."
          },
          "schema": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "name",
                "kind",
                "users"
              ],
              "properties": {
                "name": {
                  "type": "string"
                },
                "kind": {
                  "type": "string",
                  "enum": [
                    "string",
                    "number"
                  ]
                },
                "users": {
                  "type": "integer",
                  "format": "int64",
                  "description": "How many profiles carry it."
                }
              }
            }
          }
        }
      },
      "SegmentValue": {
        "type": "object",
        "description": "A typed literal. The type decides which column the comparison binds to.",
        "required": [
          "type"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "string",
              "number",
              "bool",
              "list",
              "date"
            ]
          },
          "str": {
            "type": "string"
          },
          "num": {
            "type": "number"
          },
          "num2": {
            "type": "number",
            "description": "The upper bound for `between`."
          },
          "bool": {
            "type": "boolean"
          },
          "list": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "At most 1000 values."
          },
          "date": {
            "type": "string",
            "format": "date-time"
          },
          "date2": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SegmentWindow": {
        "type": "object",
        "description": "A time range on an event condition. Absolute bounds are always UTC instants; the panel presents them on the Iranian calendar but never sends Iranian calendar components over the wire.",
        "required": [
          "kind"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "all_time",
              "last",
              "between",
              "before",
              "after"
            ]
          },
          "amount": {
            "type": "integer"
          },
          "unit": {
            "type": "string",
            "enum": [
              "minute",
              "hour",
              "day",
              "week",
              "month"
            ]
          },
          "from": {
            "type": "string",
            "format": "date-time"
          },
          "to": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SegmentNode": {
        "type": "object",
        "description": "One condition. A single struct with a `kind` discriminator rather than a union, because it round-trips through JSON on every keystroke of the segment builder. A misspelled event name is not an error. It compiles, and it matches nobody, which is the single most common way an audience comes back empty. Read `description_fa` back from /v1/audiences/validate before you point a campaign at it.",
        "required": [
          "kind"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "group",
              "trait",
              "event",
              "segment",
              "engagement",
              "churn"
            ]
          },
          "op": {
            "type": "string",
            "enum": [
              "and",
              "or"
            ],
            "description": "`group` only."
          },
          "not": {
            "type": "boolean",
            "description": "`group` only."
          },
          "children": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SegmentNode"
            },
            "description": "`group` only. Nesting is capped at depth 8 and 200 nodes overall."
          },
          "trait": {
            "type": "string",
            "description": "`trait` only. The profile attribute name."
          },
          "event": {
            "type": "string",
            "description": "`event` only. The event name."
          },
          "negate": {
            "type": "boolean",
            "description": "`event` only. Did NOT do."
          },
          "count": {
            "type": "object",
            "description": "`event` only. How many times.",
            "required": [
              "operator",
              "value"
            ],
            "properties": {
              "operator": {
                "type": "string",
                "enum": [
                  "gte",
                  "lte",
                  "eq",
                  "between"
                ]
              },
              "value": {
                "type": "number"
              },
              "value2": {
                "type": "number"
              }
            }
          },
          "aggregate": {
            "type": "object",
            "description": "`event` only. A sum or average over one of the event's properties.",
            "required": [
              "function",
              "property",
              "operator",
              "value"
            ],
            "properties": {
              "function": {
                "type": "string",
                "enum": [
                  "sum",
                  "avg",
                  "min",
                  "max"
                ]
              },
              "property": {
                "type": "string"
              },
              "operator": {
                "type": "string"
              },
              "value": {
                "type": "number"
              },
              "value2": {
                "type": "number"
              }
            }
          },
          "properties": {
            "type": "array",
            "description": "`event` only. Narrows the condition to a subset of those events.",
            "items": {
              "type": "object",
              "required": [
                "property",
                "operator"
              ],
              "properties": {
                "property": {
                  "type": "string",
                  "description": "At most 128 characters."
                },
                "operator": {
                  "type": "string"
                },
                "value": {
                  "$ref": "#/components/schemas/SegmentValue"
                }
              }
            }
          },
          "window": {
            "$ref": "#/components/schemas/SegmentWindow"
          },
          "segment_id": {
            "type": "integer",
            "format": "int64",
            "description": "`segment` only."
          },
          "in_segment": {
            "type": "boolean",
            "description": "`segment` only."
          },
          "band": {
            "type": "string",
            "description": "`engagement` and `churn`. The coarse filter, by name. Setting it uses the band; otherwise the metric comparison applies."
          },
          "metric": {
            "type": "string",
            "description": "`engagement` only: score, ignored_streak, open_rate, click_rate or days_since_engaged."
          },
          "operator": {
            "type": "string",
            "enum": [
              "eq",
              "neq",
              "contains",
              "not_contains",
              "starts_with",
              "ends_with",
              "gt",
              "gte",
              "lt",
              "lte",
              "between",
              "in",
              "not_in",
              "is_set",
              "is_not_set"
            ]
          },
          "value": {
            "$ref": "#/components/schemas/SegmentValue"
          }
        }
      },
      "SegmentDefinition": {
        "type": "object",
        "required": [
          "version",
          "root"
        ],
        "properties": {
          "version": {
            "type": "integer"
          },
          "root": {
            "$ref": "#/components/schemas/SegmentNode"
          }
        }
      },
      "AudienceRequest": {
        "type": "object",
        "required": [
          "definition"
        ],
        "properties": {
          "definition": {
            "$ref": "#/components/schemas/SegmentDefinition"
          },
          "limit": {
            "type": "integer",
            "description": "Accepted by the decoder and ignored by both handlers."
          }
        }
      },
      "ValidateResponse": {
        "type": "object",
        "required": [
          "valid",
          "description_fa"
        ],
        "properties": {
          "valid": {
            "type": "boolean",
            "description": "Always true on a 200; an invalid filter is a 422."
          },
          "description_fa": {
            "type": "string",
            "description": "The filter said back to you in Persian."
          }
        }
      },
      "CountResponse": {
        "type": "object",
        "required": [
          "count",
          "approximate",
          "description",
          "took_ms"
        ],
        "properties": {
          "count": {
            "type": "integer",
            "format": "int64",
            "description": "Exact."
          },
          "approximate": {
            "type": "boolean",
            "description": "Always false on this route."
          },
          "description": {
            "type": "string",
            "description": "The Persian sentence. Note the key: not `description_fa`."
          },
          "took_ms": {
            "type": "integer",
            "format": "int64"
          }
        }
      },
      "SavedSegment": {
        "type": "object",
        "description": "The account id is never serialised.",
        "required": [
          "id",
          "name",
          "kind",
          "definition",
          "description_fa",
          "last_size",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "name": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "dynamic",
              "static",
              "realtime"
            ],
            "description": "Everything this API creates is dynamic. A static list is a named set somebody put people into, and it cannot be created here."
          },
          "definition": {
            "$ref": "#/components/schemas/SegmentDefinition"
          },
          "description_fa": {
            "type": "string",
            "description": "Cached at save time, so a list of two hundred renders without recompiling."
          },
          "last_size": {
            "type": "integer",
            "format": "int64",
            "description": "The audience the last time it was counted, not now."
          },
          "last_computed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SegmentListResponse": {
        "type": "object",
        "required": [
          "segments"
        ],
        "properties": {
          "segments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SavedSegment"
            }
          }
        }
      },
      "SegmentWriteRequest": {
        "type": "object",
        "required": [
          "name",
          "definition"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Required on create. Blank on update keeps the existing name."
          },
          "definition": {
            "$ref": "#/components/schemas/SegmentDefinition"
          }
        }
      },
      "SegmentWriteResponse": {
        "type": "object",
        "required": [
          "id",
          "name",
          "description_fa"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "name": {
            "type": "string"
          },
          "description_fa": {
            "type": "string"
          }
        }
      },
      "CampaignSummary": {
        "type": "object",
        "required": [
          "id",
          "name",
          "channel",
          "status",
          "estimated",
          "processed",
          "sent",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "name": {
            "type": "string"
          },
          "channel": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "scheduled",
              "running",
              "paused",
              "completed",
              "cancelled",
              "failed"
            ]
          },
          "estimated": {
            "type": "integer",
            "format": "int64"
          },
          "processed": {
            "type": "integer",
            "format": "int64"
          },
          "sent": {
            "type": "integer",
            "format": "int64"
          },
          "scheduled_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CampaignListResponse": {
        "type": "object",
        "required": [
          "campaigns"
        ],
        "properties": {
          "campaigns": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CampaignSummary"
            }
          }
        }
      },
      "CampaignCreateRequest": {
        "type": "object",
        "required": [
          "template_id"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Not validated. An empty name is accepted."
          },
          "channel": {
            "type": "string",
            "description": "push, webpush, sms, email, inapp, messenger or webhook. The aliases web, p, s, e, w and i resolve, and bale, eitaa and rubika each fold into messenger. An empty string means the template decides. Anything else is a 400 naming the field."
          },
          "template_id": {
            "type": "integer",
            "format": "int64",
            "description": "Required. Inline message bodies are not accepted."
          },
          "channels": {
            "type": "array",
            "description": "Fallback channels, in order, each with its own template. The campaign tries `channel` first and moves to the next only when that medium cannot carry the message: no device, no phone number, that channel switched off. An unsubscribe, a frequency cap or a holdout stops it there rather than moving on, because those are answers about the person and not about the medium. One template cannot serve two channels: a push body sent as an SMS is what that would mean.",
            "items": {
              "type": "object",
              "required": [
                "channel",
                "template_id"
              ],
              "properties": {
                "channel": {
                  "type": "string"
                },
                "template_id": {
                  "type": "integer",
                  "format": "int64",
                  "description": "A template written for THIS channel."
                }
              }
            }
          },
          "segment_id": {
            "type": "integer",
            "format": "int64",
            "description": "Zero means the inline `definition` is the audience."
          },
          "definition": {
            "$ref": "#/components/schemas/SegmentDefinition",
            "description": "Not validated on this route, unlike a segment's."
          },
          "topic_id": {
            "type": "integer",
            "format": "int64",
            "description": "Zero means no preference-centre topic, which is always permitted."
          },
          "scheduled_at": {
            "type": "string",
            "description": "RFC 3339. An unparseable value is dropped silently and the campaign is left unscheduled."
          },
          "use_local_time": {
            "type": "boolean"
          },
          "local_hour": {
            "type": "integer",
            "description": "0 to 23, checked only when `use_local_time` is true."
          },
          "throttle_minutes": {
            "type": "integer",
            "description": "Not validated."
          },
          "control_group_pct": {
            "type": "number",
            "description": "0 to 100."
          },
          "goal_event": {
            "type": "string",
            "description": "Empty means order_completed at read time."
          }
        }
      },
      "CampaignCreateResponse": {
        "type": "object",
        "required": [
          "id",
          "status"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "status": {
            "type": "string"
          }
        }
      },
      "CampaignRecurrence": {
        "type": "object",
        "required": [
          "cadence"
        ],
        "properties": {
          "cadence": {
            "type": "string",
            "enum": [
              "daily",
              "weekly",
              "monthly",
              "yearly"
            ]
          },
          "hour": {
            "type": "integer",
            "minimum": 0,
            "maximum": 23,
            "description": "Tehran hour. Ignored when hours is set."
          },
          "hours": {
            "type": "array",
            "maxItems": 6,
            "items": {
              "type": "integer",
              "minimum": 0,
              "maximum": 23
            },
            "description": "Tehran hours. When present, replaces hour."
          },
          "day_of_week": {
            "type": "integer",
            "minimum": 0,
            "maximum": 6,
            "description": "Saturday is 0. Weekly only."
          },
          "day_of_month": {
            "type": "integer",
            "minimum": 1,
            "maximum": 31,
            "description": "Jalali day. Monthly and yearly. A day past the end of a short month lands on that month's last day rather than rolling into the next, so 30 Esfand sends on 29 Esfand in a common year."
          },
          "month": {
            "type": "integer",
            "minimum": 1,
            "maximum": 12,
            "description": "Jalali month, 1 for Farvardin. Yearly only, and required for it."
          },
          "ends_at": {
            "type": "string",
            "format": "date-time"
          },
          "max_occurrences": {
            "type": "integer",
            "minimum": 0,
            "description": "Zero means no count limit."
          },
          "occurrences": {
            "type": "integer",
            "readOnly": true,
            "description": "Worker-owned and reset when a schedule is saved."
          }
        }
      },
      "CampaignRecurrenceRequest": {
        "type": "object",
        "required": [
          "recurrence"
        ],
        "properties": {
          "recurrence": {
            "$ref": "#/components/schemas/CampaignRecurrence"
          }
        }
      },
      "CampaignRecurrenceResponse": {
        "type": "object",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "note": {
            "type": "string",
            "description": "Present when clearing a schedule."
          }
        }
      },
      "CampaignSubmitResponse": {
        "type": "object",
        "required": [
          "approval_id",
          "state",
          "fingerprint"
        ],
        "properties": {
          "approval_id": {
            "type": "integer",
            "format": "int64"
          },
          "state": {
            "type": "string",
            "enum": [
              "pending",
              "approved",
              "rejected",
              "stale"
            ]
          },
          "fingerprint": {
            "type": "string",
            "description": "32 hex characters over the audience, template, channel and variants."
          }
        }
      },
      "CampaignReport": {
        "type": "object",
        "required": [
          "campaign",
          "progress",
          "percent",
          "reach",
          "delivery",
          "engagement",
          "engagement_rejects"
        ],
        "properties": {
          "campaign": {
            "type": "object",
            "description": "The stored campaign. Note that `tenant_id` IS on the wire here, unlike on a segment.",
            "properties": {
              "id": {
                "type": "integer",
                "format": "int64"
              },
              "tenant_id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "created_by": {
                "type": "integer"
              },
              "segment_id": {
                "type": "integer",
                "format": "int64"
              },
              "definition": {
                "$ref": "#/components/schemas/SegmentDefinition"
              },
              "template_id": {
                "type": "integer",
                "format": "int64"
              },
              "channel": {
                "type": "string"
              },
              "channels": {
                "type": "array",
                "items": {
                  "type": "object"
                }
              },
              "channel_mode": {
                "type": "string"
              },
              "topic_id": {
                "type": "integer",
                "format": "int64"
              },
              "status": {
                "type": "string"
              },
              "scheduled_at": {
                "type": "string",
                "format": "date-time"
              },
              "use_local_time": {
                "type": "boolean"
              },
              "local_hour": {
                "type": "integer"
              },
              "throttle_minutes": {
                "type": "integer"
              },
              "budget": {
                "type": "string"
              },
              "min_interval_hours": {
                "type": "integer"
              },
              "priority": {
                "type": "integer"
              },
              "control_group_pct": {
                "type": "number"
              },
              "goal_event": {
                "type": "string"
              },
              "variants": {
                "type": "array",
                "items": {
                  "type": "object"
                }
              }
            }
          },
          "progress": {
            "type": "object",
            "properties": {
              "campaign_id": {
                "type": "integer",
                "format": "int64"
              },
              "cursor": {
                "type": "string"
              },
              "estimated": {
                "type": "integer",
                "format": "int64"
              },
              "processed": {
                "type": "integer",
                "format": "int64"
              },
              "sent": {
                "type": "integer",
                "format": "int64"
              },
              "suppressed": {
                "type": "integer",
                "format": "int64"
              },
              "deferred": {
                "type": "integer",
                "format": "int64"
              },
              "failed": {
                "type": "integer",
                "format": "int64"
              },
              "holdout": {
                "type": "integer",
                "format": "int64"
              },
              "started_at": {
                "type": "string",
                "format": "date-time"
              },
              "updated_at": {
                "type": "string",
                "format": "date-time"
              },
              "finished_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          },
          "percent": {
            "type": "number",
            "description": "A finished run is always 100 and an unstarted one 0. It clamps, because the estimate is sampled."
          },
          "reach": {
            "type": "array",
            "description": "Why the audience and the send count differ.",
            "items": {
              "type": "object",
              "properties": {
                "status": {
                  "type": "string"
                },
                "reason": {
                  "type": "string"
                },
                "reason_fa": {
                  "type": "string"
                },
                "count": {
                  "type": "integer",
                  "format": "int64"
                }
              }
            }
          },
          "delivery": {
            "type": "array",
            "description": "Empty for channels nobody can confirm. An empty section beats zeros that read as total failure.",
            "items": {
              "type": "object",
              "properties": {
                "delivery": {
                  "type": "string"
                },
                "delivery_fa": {
                  "type": "string"
                },
                "count": {
                  "type": "integer",
                  "format": "int64"
                }
              }
            }
          },
          "engagement": {
            "type": "array",
            "description": "Per channel, never blended: an open means something different on each one.",
            "items": {
              "type": "object",
              "properties": {
                "channel": {
                  "type": "string"
                },
                "channel_fa": {
                  "type": "string"
                },
                "issued": {
                  "type": "integer",
                  "format": "int64"
                },
                "withheld": {
                  "type": "integer",
                  "format": "int64"
                },
                "measurable_open": {
                  "type": "integer",
                  "format": "int64",
                  "description": "How many sends could ever have reported an open."
                },
                "measurable_click": {
                  "type": "integer",
                  "format": "int64"
                },
                "opened": {
                  "type": "integer",
                  "format": "int64"
                },
                "clicked": {
                  "type": "integer",
                  "format": "int64"
                },
                "opened_unmeasurable": {
                  "type": "integer",
                  "format": "int64"
                },
                "clicked_unmeasurable": {
                  "type": "integer",
                  "format": "int64"
                },
                "why_open": {
                  "type": "string",
                  "description": "Why opens are not measurable on this channel, in Persian."
                },
                "why_click": {
                  "type": "string"
                }
              }
            }
          },
          "engagement_rejects": {
            "type": "array",
            "description": "Claims we refused to believe, shown rather than logged.",
            "items": {
              "type": "object",
              "properties": {
                "verdict": {
                  "type": "string"
                },
                "verdict_fa": {
                  "type": "string"
                },
                "detail": {
                  "type": "string"
                },
                "count": {
                  "type": "integer",
                  "format": "int64"
                }
              }
            }
          },
          "uplift": {
            "type": "object",
            "description": "Absent until the attribution window closes. `verdict` is one of too_early, no_control, inconclusive, positive, negative, contaminated. The point estimate is never readable without its bounds: it is not a measurement, it is the middle of a range. `money_known` is false when conversions arrived in more than one currency, because rial and toman differ by a factor of ten with nothing upstream converting between them.",
            "properties": {
              "verdict": {
                "type": "string"
              },
              "verdict_fa": {
                "type": "string"
              },
              "goal": {
                "type": "string"
              },
              "treated_users": {
                "type": "integer",
                "format": "int64"
              },
              "treated_conversions": {
                "type": "integer",
                "format": "int64"
              },
              "control_users": {
                "type": "integer",
                "format": "int64"
              },
              "control_conversions": {
                "type": "integer",
                "format": "int64"
              },
              "contaminated": {
                "type": "integer",
                "format": "int64"
              },
              "lift": {
                "type": "number"
              },
              "lift_low": {
                "type": "number"
              },
              "lift_high": {
                "type": "number"
              },
              "extra_low": {
                "type": "number"
              },
              "extra": {
                "type": "number"
              },
              "extra_high": {
                "type": "number"
              },
              "median_order": {
                "type": "number"
              },
              "currency": {
                "type": "string"
              },
              "extra_revenue": {
                "type": "number"
              },
              "extra_revenue_low": {
                "type": "number"
              },
              "extra_revenue_high": {
                "type": "number"
              },
              "money_known": {
                "type": "boolean"
              },
              "needed_per_arm": {
                "type": "integer",
                "format": "int64",
                "description": "How large each arm would have to be to tell this effect from noise."
              },
              "window_closed_at": {
                "type": "string",
                "format": "date-time"
              },
              "computed_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        }
      },
      "IngestEventsRequest": {
        "type": "object",
        "required": [
          "events"
        ],
        "properties": {
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Envelope"
            },
            "description": "Between 1 and 500. Each item needs its own `type`."
          }
        }
      },
      "IngestEventsResponse": {
        "type": "object",
        "required": [
          "accepted"
        ],
        "properties": {
          "accepted": {
            "type": "integer"
          },
          "rejected": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "index",
                "reason"
              ],
              "properties": {
                "index": {
                  "type": "integer"
                },
                "reason": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "ExportQueueRequest": {
        "type": "object",
        "required": [
          "kind"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "events",
              "messages",
              "profiles",
              "segment"
            ]
          },
          "format": {
            "type": "string",
            "description": "Anything that is not exactly `csv` becomes `ndjson`, including a typo."
          },
          "spec": {
            "type": "object",
            "additionalProperties": true,
            "description": "Passed through untouched and unvalidated. Its keys differ per kind and nothing in this repository enumerates them."
          }
        }
      },
      "ExportQueueResponse": {
        "type": "object",
        "required": [
          "id",
          "status",
          "kind",
          "expires_after_hours"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "status": {
            "type": "string"
          },
          "kind": {
            "type": "string"
          },
          "expires_after_hours": {
            "type": "integer",
            "description": "168, seven days. A file holding every customer's email address sitting on a share for ever is what turns one careless export into a breach, and nobody remembers to delete it."
          }
        }
      },
      "ExportJob": {
        "type": "object",
        "required": [
          "id",
          "kind",
          "format",
          "status",
          "rows_written",
          "bytes",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "kind": {
            "type": "string"
          },
          "format": {
            "type": "string"
          },
          "spec": {
            "type": "object",
            "additionalProperties": true
          },
          "status": {
            "type": "string"
          },
          "rows_written": {
            "type": "integer",
            "format": "int64"
          },
          "bytes": {
            "type": "integer",
            "format": "int64"
          },
          "location": {
            "type": "string",
            "description": "A storage path, not a signed URL."
          },
          "error": {
            "type": "string"
          },
          "attempts": {
            "type": "integer",
            "description": "'It failed' and 'it failed three times and stopped' are different answers to the only question anybody asks about a queued export."
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "requested_by": {
            "type": "string",
            "description": "`api-key:<id>` when this API queued it."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "finished_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "ExportListResponse": {
        "type": "object",
        "required": [
          "data",
          "has_more"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ExportJob"
            }
          },
          "next_cursor": {
            "type": "string",
            "description": "Never set by this handler."
          },
          "has_more": {
            "type": "boolean",
            "description": "Always false, even when there are more rows."
          }
        }
      },
      "AnalyticsTimeRange": {
        "type": "object",
        "required": [
          "from",
          "to"
        ],
        "properties": {
          "from": {
            "type": "string",
            "format": "date-time"
          },
          "to": {
            "type": "string",
            "format": "date-time",
            "description": "The span must be under 730 days."
          }
        }
      },
      "AnalyticsStep": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "The event name, at most 256 characters. Empty means any activity, on retention only."
          },
          "label": {
            "type": "string",
            "description": "What the chart calls this step. Marketers name stages differently from engineers."
          },
          "filters": {
            "type": "array",
            "description": "At most 10.",
            "items": {
              "type": "object",
              "required": [
                "prop",
                "op",
                "value"
              ],
              "properties": {
                "prop": {
                  "type": "string",
                  "description": "At most 128 characters."
                },
                "op": {
                  "type": "string",
                  "enum": [
                    "eq",
                    "ne",
                    "contains",
                    "prefix",
                    "gt",
                    "gte",
                    "lt",
                    "lte",
                    "num_eq",
                    "num_ne"
                  ],
                  "description": "The first four compare text, the rest compare numbers. The same key can hold either."
                },
                "value": {
                  "type": "string",
                  "description": "At most 512 characters, and always a string, even for a numeric comparison."
                }
              }
            }
          }
        }
      },
      "FunnelRequest": {
        "type": "object",
        "required": [
          "steps",
          "range",
          "window"
        ],
        "properties": {
          "steps": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AnalyticsStep"
            },
            "description": "Between 2 and 12."
          },
          "range": {
            "$ref": "#/components/schemas/AnalyticsTimeRange"
          },
          "window": {
            "type": "string",
            "description": "How long somebody has to finish. Required and greater than zero. A Go duration, with a `d` suffix allowed: 7d, 1.5d, 36h. Must not exceed the range."
          },
          "strict": {
            "type": "boolean",
            "description": "Requires the steps with nothing in between. Off by default: 'how many who viewed a product bought it' does not mean 'and did nothing else in between'."
          },
          "split_by": {
            "type": "string",
            "description": "One of platform, os, device, app_version, country, city, region, province, utm_source, utm_campaign, browser, or `prop:` followed by any property key of yours."
          }
        }
      },
      "FunnelStep": {
        "type": "object",
        "required": [
          "index",
          "name",
          "label",
          "users",
          "from_start",
          "from_previous",
          "dropped_here"
        ],
        "properties": {
          "index": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "users": {
            "type": "integer",
            "format": "int64"
          },
          "from_start": {
            "type": "number",
            "description": "A fraction, not a percentage."
          },
          "from_previous": {
            "type": "number",
            "description": "Which stage is broken, rather than which is small."
          },
          "dropped_here": {
            "type": "integer",
            "format": "int64",
            "description": "The loss, which is the number a marketer acts on."
          }
        }
      },
      "FunnelResult": {
        "type": "object",
        "required": [
          "steps",
          "entered",
          "completed",
          "conversion",
          "description"
        ],
        "properties": {
          "steps": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FunnelStep"
            }
          },
          "buckets": {
            "type": "array",
            "description": "Present only when `split_by` was given.",
            "items": {
              "type": "object",
              "properties": {
                "value": {
                  "type": "string"
                },
                "steps": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/FunnelStep"
                  }
                },
                "entered": {
                  "type": "integer",
                  "format": "int64"
                },
                "completed": {
                  "type": "integer",
                  "format": "int64"
                },
                "conversion": {
                  "type": "number"
                }
              }
            }
          },
          "entered": {
            "type": "integer",
            "format": "int64"
          },
          "completed": {
            "type": "integer",
            "format": "int64"
          },
          "conversion": {
            "type": "number"
          },
          "description": {
            "type": "string",
            "description": "The report said back in Persian."
          }
        }
      },
      "RetentionRequest": {
        "type": "object",
        "required": [
          "range"
        ],
        "properties": {
          "start": {
            "$ref": "#/components/schemas/AnalyticsStep",
            "description": "An empty name means any activity."
          },
          "return": {
            "$ref": "#/components/schemas/AnalyticsStep",
            "description": "An empty name means any activity."
          },
          "range": {
            "$ref": "#/components/schemas/AnalyticsTimeRange"
          },
          "granularity": {
            "type": "string",
            "enum": [
              "day",
              "week",
              "month"
            ],
            "default": "day"
          },
          "periods": {
            "type": "integer",
            "description": "Defaults to 30 when absent or zero. At most 60."
          }
        }
      },
      "RetentionCell": {
        "type": "object",
        "required": [
          "period",
          "users",
          "rate",
          "observable"
        ],
        "properties": {
          "period": {
            "type": "integer"
          },
          "users": {
            "type": "integer",
            "format": "int64"
          },
          "rate": {
            "type": "number",
            "description": "A fraction."
          },
          "observable": {
            "type": "boolean",
            "description": "False means the report has not run long enough to know yet, which is not the same as zero."
          }
        }
      },
      "RetentionResult": {
        "type": "object",
        "required": [
          "granularity",
          "period_label",
          "cohorts",
          "average",
          "description"
        ],
        "properties": {
          "granularity": {
            "type": "string"
          },
          "period_label": {
            "type": "string",
            "description": "Persian."
          },
          "cohorts": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "cohort",
                "label",
                "size",
                "cells"
              ],
              "properties": {
                "cohort": {
                  "type": "string",
                  "description": "The cohort's start, on the Iranian calendar."
                },
                "label": {
                  "type": "string"
                },
                "size": {
                  "type": "integer",
                  "format": "int64"
                },
                "cells": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/RetentionCell"
                  }
                }
              }
            }
          },
          "average": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RetentionCell"
            },
            "description": "Weighted, not a mean of percentages."
          },
          "description": {
            "type": "string",
            "description": "Persian."
          }
        }
      },
      "TransactionalRequest": {
        "type": "object",
        "description": "Unknown fields are refused on this route, and only on this route.",
        "required": [
          "user_id",
          "channel",
          "template_id",
          "idempotency_key"
        ],
        "properties": {
          "user_id": {
            "type": "string"
          },
          "channel": {
            "type": "string",
            "enum": [
              "push",
              "sms",
              "email",
              "webpush",
              "inapp",
              "bale",
              "eitaa",
              "rubika"
            ],
            "description": "Compared against this list literally. `web` is refused here even though it resolves elsewhere, and `messenger` and `webhook` are campaign-only."
          },
          "category": {
            "type": "string",
            "enum": [
              "transactional",
              "critical"
            ],
            "default": "transactional",
            "description": "`marketing` is refused with a 400. Use a campaign for that."
          },
          "template_id": {
            "type": "integer",
            "format": "int64",
            "description": "Required. Message text is never accepted inline."
          },
          "vars": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "At most 40. Unlike a campaign, where values come from the stored profile, these carry what only you know: the order number, the code, the courier."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Required, 8 to 200 characters of letters, digits, dot, dash, underscore or colon. Case is preserved, because folding would merge 'Order-8821' with 'order-8821', two keys a strict caller may be using for two different things. It becomes part of the message id, which is why the character set is strict. Keys are never swept, so a key reused a year later replays the year-old answer rather than sending again."
          }
        }
      },
      "TransactionalResult": {
        "type": "object",
        "required": [
          "message_id",
          "status",
          "sent_at"
        ],
        "properties": {
          "message_id": {
            "type": "string",
            "description": "Derived, not generated: `t` plus your account id, a dot, and your key."
          },
          "status": {
            "type": "string"
          },
          "reason": {
            "type": "string",
            "description": "Set when the message was deliberately not sent: an opt-out, a suppression, no address."
          },
          "reason_fa": {
            "type": "string",
            "description": "The same reason in Persian, ready to show."
          },
          "error": {
            "type": "string"
          },
          "replayed": {
            "type": "boolean",
            "description": "Served from the ledger rather than freshly sent."
          },
          "sent_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    }
  }
}
