Concepts: events, profiles, segments, campaigns, journeys
The ten ideas the rest of the documentation stands on, one paragraph each, named the way the panel and the API name them.
This page is ten words and no more. Each one is a specific thing in the product, and the name is the same in the panel, in the API and in this documentation. If an AI agent is going to work against this service, this is the first page it should read.
Where this page says something does not exist, it does not exist. It is not a gap in the documentation.
| Concept | In the panel | On the wire and in Persian |
|---|---|---|
| event | Events | رویداد |
| profile | Profile, and Users in the navigation | پرونده |
| trait | Trait | ویژگی |
| segment | Segments | سگمنت |
| audience | Audience | مخاطب |
| campaign | Campaigns | کمپین |
| journey | Journeys | سناریو |
| channel | Channels | کانال |
| write key | Write key | کلید نوشتن |
| API key | API keys | کلید API |
Event
An event is something that happened once, at a moment, to one identity. It has a name, a time, and it can carry properties. There are five message types and the request path decides which one you get: track, identify, page, screen and alias. There are two places you write the type yourself: the items of POST /v1/batch on the ingest host, and the items of POST /v1/events on the management host. In both, a type we do not recognise rejects that item with unknown_type.
An event name is at most 128 bytes after Persian normalisation and may hold no control characters. There is no allow-list, no case folding and no snake_case enforcement, and a Persian name is accepted. The direct consequence is that Order Completed and order_completed stay two different events for ever, and nothing warns you. Eleven standard names exist, and they are what the ready-made funnels and journeys are built on: product_viewed, product_added_to_cart, product_removed_from_cart, cart_viewed, checkout_started, order_completed, order_refunded, order_cancelled, searched, signed_up and signed_in.
At most 256 properties are kept per event. Keys are normalised: whitespace, full stops and hyphens all become underscores. Values land in two places, a string map and a numeric map, because a map in the warehouse has to be of one type. A null value is not stored at all, because "not set" and "the empty string" are different things, and if they were the same, an is not set filter would give the wrong answer.
What an event is not: it is not a state. "This user now has a gold subscription" is a trait; "this user bought a gold subscription" is an event. A stored event is also not editable. There is no endpoint to edit or delete one, and three things remove a row: your tenant's retention policy, the events table's own 400-day TTL, and the erasure of one named person.
Profile and trait
A profile is one row per user_id per account, folded together from that user's events. In the panel's navigation the section is called Users, and the pages themselves say profile. A trait is something that is true about that person: an email address, a city, a balance, a signup date. Traits arrive on identify.
A few traits behave specially. email is trimmed and lower-cased and nothing else. phone is converted to E.164 and a derived phone_operator is written beside it. national_id is dropped entirely when its check digit fails. gender is mapped to one of male, female or other. Every other trait is free-form and is written twice, once as a string and once as a number. Only the numeric half answers a "greater than" condition. That double write came out of a real failure in which "balance of 100 or more" returned nobody and "less than 10" returned all 114,943 profiles including one holding 428, with no error and no warning.
Not mentioning a trait means "leave it alone". Sending an empty string also means "leave it alone" rather than "delete it": the empty value is dropped before the event is built and never reaches the profile. The profile layer does carry a "clear this" rule and no customer-facing route reaches it, and a journey's update-trait node refuses an empty value by name. So there is no supported way to delete one trait, and the only real removal is the personal-data erasure flow, which takes the whole profile. A new profile starts with all three sending consents (email, SMS, push) switched on, because a customer base arriving on the platform has already consented through the customer's own flow, and defaulting to off means the first campaign reaches nobody.
What a profile is not: an anonymous visitor does not have one. No row is created until an event carries a user_id.
Identity
There are three identifiers. The SDK mints and keeps anonymous_id. Your own authentication supplies user_id. previous_id means something only on an alias message and lives for that one message. At least one of the first two is mandatory, and whichever is in force is the partition key, so every message about one person is seen in order.
An alias does not move anonymous history onto the user. It does exactly one thing: it writes a row into the identity map saying that this anonymous id belonged to this user. Events already stored are never rewritten, so those rows keep an empty user_id for ever, and no report joins the identity map. A funnel that starts with an anonymous product_viewed and ends with a signed-in order_completed does not see the two as one person. The one live consumer of the identity map is the personal-data erasure path, which uses it to find and delete that person's pre-signin events as well.
Call reset() on sign-out. Without it, the next person on a shared device inherits the previous person's anonymous id, and the new identity-map row replaces the old one. The consequence is that if the first person later asks to be erased, their anonymous events are not found.
Segment
A segment is the saved object: a named filter with an id that lives in the panel and that campaigns and journeys refer to. Its definition is not SQL, it is a JSON tree. The interface never sends SQL, and that boundary is what makes the feature safe to put in a marketer's hands.
Three kinds are defined in the database. dynamic is the default and stores nothing: the definition is run fresh every time somebody counts it or a campaign pages through it. static is a list somebody put people into, an agency's spreadsheet or the winners of a draw, and its membership is real rows. A dynamic segment needs a definition; a static one does not. The third kind, realtime, is accepted by both the API and the database constraint, and no part of the server implements it. Treat it as reserved rather than working.
What a segment is not: a dynamic segment has no stored membership and no refresh job. Its last size and last computed time are never written on a real install, so the card in the segment library permanently reads that the size has not been computed. A size is calculated at the moment you ask for it.
Audience
An audience is the set of people a send reaches. Choosing a campaign's recipients in the panel is exactly this. On the management API the same word names two stateless routes: POST /v1/audiences/validate, which only says whether your definition compiles, and POST /v1/audiences/count, which returns a number. Both take the definition in the body and store nothing.
The practical difference is this: a segment is the name of the thing you save, an audience is the name of the set of people a filter resolves to, whether or not that filter was saved. Both words describe one filter language.
The double naming is real in the product and we are not hiding it: the counting route is /v1/audiences/count while the saved object lives under /v1/segments and the panel calls it a segment. There is no estimate route on the management API. The panel's live counter calls POST /v1/segments/estimate, which is registered on the dashboard's control plane, and that port is deliberately not routed from the internet. From your own server the route that answers is POST /v1/audiences/count, and it counts exactly rather than sampling.
Campaign
A campaign is one message to one audience, once or on a schedule. Its states are draft, scheduled, running, paused, completed, cancelled and failed. The last three are terminal and there is no way back from them.
On the management API you can list campaigns, read one, create one, send one, and submit one for approval. The list is the two hundred most recently updated campaigns and no more. That cap is not announced in the response: there is no count, no has_more and no cursor. An account holding a two hundred and first campaign does not see it on this surface and has no way to page past. The same is true of GET /v1/segments. Pause, resume and cancel are not on that surface at all; they exist only in the panel. A state change that matches no row answers 404, and the three separate causes (not yours, does not exist, wrong state) deliberately give one answer.
What a campaign is not: it is not a journey. A campaign resolves its audience once and walks through it. A journey holds state for each person separately.
Journey
A journey is a graph that each user walks alone, with their own state. The Persian word is سناریو, and it is the only one: the panel used to say ژورنی on a handful of dashboard screens, which read as a second concept rather than the same one.
There are eight node kinds: trigger, wait, condition, switch, split, action, goal and exit. The entry rule is one of once, every_time or max_n. Exit criteria remove somebody the moment the criteria match, wherever in the graph they are. Published versions are immutable, and their segment definitions are copied inline rather than referenced, so a published version means today what it meant on the day it was published.
What a journey is not: there is no journey route on the management API at all. GET /v1/capabilities reports a journeys key under features, but that flag says only whether this deployment has the journey subsystem wired, and even when it is true it turns on no route on that host. None of the features flags is a fixed value: they differ from one installation to the next, so ask for them at runtime rather than writing them into your code. Building, publishing and pushing people in are all panel-only operations.
Channel
A channel is the route a message takes. The strings that exist are push (mobile push), webpush (browser push), sms, email, inapp (the in-app inbox), messenger, and the three messengers bale, eitaa and rubika.
You do not write the three messengers on a campaign directly: each is promoted to messenger, and at send time it is resolved per recipient. Whether a channel is available is computed from account configuration alone, with nothing dialled: email, for instance, is available when both an SMTP host and a from-address are set. An unavailable channel is listed with its reason rather than hidden.
One hole, stated plainly: webhook is accepted as a channel on a campaign, and no sender was ever built for it. A campaign authored on webhook fails for the whole audience. If what you want is a journey calling your own service, that is an action kind inside a journey, not this channel.
Write key against management key
There are two kinds of key and neither works where the other one does.
| Write key | API key | |
|---|---|---|
| Prefix | wk_seg_ | sk_seg_ |
| Host | https://in.segmentic.net | https://api.segmentic.net |
| Where it lives | inside the public code of a site or app | server side only |
| What it can do | write events, register a device, subscribe to push, fetch on-site campaigns | whatever its role and permissions allow |
| What it cannot do | read a profile, mint another key | be used on the ingest host |
| Where it is created | SDK Setup | Settings, then API keys |
Both are shown in full exactly once and only a hash is stored afterwards. The two prefixes are separate so that secret scanners recognise a leaked key, and so that the severity of a leak can be judged at a glance.
A write key sent to the management API answers 401 with its own code, write_key_rejected, and a message saying that this is an SDK key and this API needs a management key. It has a dedicated code because the mistake is common and a generic answer sends people looking for the wrong problem. The reverse mistake has no dedicated code.
Permissions on an API key come from its role. A key with the owner role cannot be created at all, so no key can ever transfer or delete an account. The column that would narrow a key below its role exists in the database and no code writes it, which means every key the product creates today carries its whole role, and scoped in the GET /v1/whoami response is always false.
The last difference is consumption. The management API gives each key a weighted budget per calendar minute, 600 units by default: a whoami costs one unit and a retention report costs twenty-five. The ingest host has no rate limiting at all.