Quickstart: from key to first event
Create a key, send your first event and watch it arrive, in ten minutes and with one HTTP request.
This page needs a Segmentic account and a terminal. No SDK, no npm, no library. By the end you will have sent an event, watched it arrive in the panel, and attached it to a named person.
It starts in a browser, and that is said first because it is not what a reader expects: no public HTTP route mints a key. The two routes that do sit on the panel's internal listener, which nothing outside the network can address; the panel reaches it for you, carrying your signed-in session. So the first key comes out of a browser. There is no self-service signup either; an operator creates the account and you start with a username and a password. The honest shape of the path is two turns in a browser and one request in a terminal.
Everything you send here is real data on your account. If you would rather keep test data away from real data, create a separate app for it, which is the next step anyway.
Create a key
Sign in and go to https://app.segmentic.net/en/connect. In the sidebar this page sits under Settings and is called SDK Setup. The page itself is titled Connect and has four steps in order: App, Key, Install, Check.
Step one, the app. Give it a name (for example "main site"), pick a platform and press Add. The platforms you can pick are web, android, ios, windows, macos, linux and server. Every site or app becomes its own row, because every row carries its own key and revoking one does not take the rest down.
Step two, the key. On the app you just created, press New key. What comes back starts with wk_seg_ and carries forty-three characters after it, fifty in all. Copy it now, because the full key is displayed exactly once. If you lose it, mint a new one; there is no way to see the old one again and that is deliberate. From that moment only a hash of the key is stored, not the key.
This key sits in your site's public code and can only write events. It cannot read a profile and it cannot mint another key. The key the management API needs is a different thing, starts with sk_seg_, and is created somewhere else (Settings, then API keys). Sent to in.segmentic.net it answers 401.
The form on this page sends a name and a platform and nothing else, so an app created here is always development. The other two values, staging and production, are set only in the body of POST /v1/apps, which sits on the panel's internal listener beside the key routes: neither the ingest host nor the management host serves it, and the only public path to it is the panel's own server-side proxy at https://app.segmentic.net/api/proxy/v1/apps, on the signed-in user's session cookie. So a signed-in user who can mint keys can create a production app even though this form will not, and no route changes an app's environment after it is created. It makes no difference to the data either way: the environment is stored on the app and the collector never reads it. An event from a development app lands exactly where an event from a production app lands. Keeping data apart means a separate app with a separate key, not a different environment on one app.
Your first event
Put your own key in place of wk_seg_... and run this in a terminal:
curl -X POST https://in.segmentic.net/v1/track \
-H "Authorization: Bearer wk_seg_..." \
-H "Content-Type: application/json" \
-d '{
"message_id": "qs-1",
"event": "install_check",
"anonymous_id": "quickstart-1",
"properties": { "source": "curl" }
}'
The response:
{ "status": "ok", "accepted": 1 }
accepted is the number of events taken. It is omitted entirely when it is zero, so its absence means nothing was accepted.
A few things about that one request:
- The path decides the message type.
/v1/trackcan only produce atrackevent, and atypefield in the body is ignored rather than honoured. - One of
user_idoranonymous_idis required. With neither, the answer is 400 with the messagemissing_identity. - On
track,eventis required. Without it the answer is 400 withmissing_event_name. timestampis optional, and if you omit it the server's receive time is recorded. If you do send one it must beRFC 3339. Unix seconds or a bare date fail the JSON decode and answer 400.- A timestamp outside the window is moved, not refused. Older than the account's retention window it is clamped to that window's edge with a
timestamp_too_oldwarning; more than an hour ahead it is clamped to the receive time withtimestamp_in_future. Both answer 200, so if you do not read the warnings you never find out. - The account id and the app id are read from the key, never from the body. The
ipand the user agent come from the connection too, so a client cannot fake its own geography or its own device. - Instead of the
Authorizationheader the key can go inX-Segmentic-Keyor in a?write_key=query parameter. The third exists for image beacons andsendBeaconcalls, which cannot set headers.
The warning in the response
Now send the same request with no message_id:
curl -X POST https://in.segmentic.net/v1/track \
-H "Authorization: Bearer wk_seg_..." \
-H "Content-Type: application/json" \
-d '{
"event": "install_check",
"anonymous_id": "quickstart-1"
}'
It is still 200, but something comes with it:
{
"status": "ok",
"accepted": 1,
"warnings": [
{
"code": "generated_message_id",
"field": "message_id",
"note": "no message_id sent; retries of this event cannot be de-duplicated"
}
]
}
A warning means the event was accepted and something was corrected. Here the server minted an id for you, and the reason it complains is this: a message_id is what makes a retry safe. An SDK on a mobile network resends on the smallest interruption, and without a stable id a customer's purchase count silently doubles. The server generates one so the event is not lost, but flags that the sender is at fault.
De-duplication is scoped to your account and the window is 48 hours by default. Send the same message_id again and you get 200 and accepted: 1 again, with no second event stored. The answer is deliberately identical: an SDK that got an error would keep retrying for ever. A duplicate is also not metered and never appears on an invoice.
When the answer is not 200
| Status | Body | Meaning |
|---|---|---|
| 400 | {"status":"error","message":"malformed JSON"} | the body is not valid JSON |
| 400 | {"status":"error","message":"missing_identity"} | neither user_id nor anonymous_id. The message is the code itself |
| 401 | {"status":"error","message":"missing write key"} | no key in any header and none in the query |
| 401 | {"status":"error","message":"invalid write key"} | unknown key, revoked key, or a suspended account. All three give one answer, so the endpoint cannot be used to probe which keys exist |
| 402 | {"status":"error","message":"..."} with a Persian sentence | an account ceiling is full. Nothing changes until somebody makes a commercial decision, so retrying is pointless |
| 413 | {"status":"error","message":"request body too large"} | the body was over 5 MiB (5242880 bytes) |
| 503 | {"status":"error","message":"cannot verify the write key right now; retry"} | the database was unreachable for the key lookup. A Retry-After: 5 header comes with it |
| 503 | {"status":"error","message":"temporarily unavailable, please retry"} | both the message bus and the on-disk buffer failed |
The split between 401 and 503 is deliberate and came out of a real failure. An SDK reads 401 as permanent and discards the event; it reads 503 as transient and keeps it. Postgres was once scaled to zero and the key lookup answered 401, so eight events out of eight were discarded while the on-disk buffer existed precisely for that case. A failure on our side is now always a 503.
The 402 message is always Persian. This host has no locale middleware, so Accept-Language has no effect on it.
There is no rate limiting anywhere on the ingest host. The only volume control is the plan's event allowance, and that answers 402.
Every response, 200 or error, carries an X-Segmentic-Trace header: sixteen hex characters that find that one request in our logs. It appears in no error body, so it has to be taken off the header at the time. Send a valid one yourself and it is echoed back, so both sides hold the same id. It is the value to quote when you open a ticket.
Watching it arrive
There are two places to see the event, and they answer two different questions.
Step four of the Connect page, Check. It polls every four seconds on its own, so there is no need to refresh. What it answers is small and sufficient: has this app ever delivered an event, when, and how many in the last 24 hours. It also shows the last few event names, so you can see that what arrived is what you sent. Zero is a real answer there rather than an empty screen.
Live events, at https://app.segmentic.net/en/debug. In the sidebar it is under Connections & Integrations and is called Live connection test. Each row shows the event name, the user id, the values that were sent, and any warnings that event raised.
The order matters: start the recording on the Live events page first, then send. While nobody is watching, the collector writes nothing to the debugger, because one write per event for a screen nobody has open costs more than the write is worth. Closing the page stops the recording.
Live events records the single-event routes only: /v1/track, /v1/identify, /v1/page, /v1/screen and /v1/alias. POST /v1/batch is not recorded, and every SDK we ship sends over exactly that route. So after an SDK install this page stays empty even when the events are arriving perfectly. For an SDK install, take the answer from the Check step on the Connect page instead.
If the event you sent with curl never appears here, the problem is in sending it, not in the reports.
Attaching the event to a person
So far the event belongs to quickstart-1, which is an anonymous id. identify is what creates a named person with traits:
curl -X POST https://in.segmentic.net/v1/identify \
-H "Authorization: Bearer wk_seg_..." \
-H "Content-Type: application/json" \
-d '{
"message_id": "qs-2",
"user_id": "u_123",
"anonymous_id": "quickstart-1",
"traits": {
"email": "Ali@Digikala.COM",
"phone": "09123456789",
"city": "شيراز",
"key_balance": 428
}
}'
The response:
{ "status": "ok", "accepted": 1 }
Four things happened that the response does not show:
emailwas trimmed and lower-cased toali@digikala.com. There is no format validation on an email address at all.phonewas stored as E.164,+989123456789, and a second traitphone_operatorwas written with the valuemci. Had the number not parsed, the raw value would have been stored as given,phone_operatorwould not have been written, and aninvalid_phonewarning would have come back.citywas normalised: Arabic ye folded to Persian ye. Without that, a segment filtering on «شیراز» silently misses every user whose keyboard was Arabic.key_balancewas written twice: once as the string"428"and once as the number428. The numeric half is what answers a condition like "greater than 100". A numeric-looking string is never parsed into a number, because"0912..."would lose its leading zero and a national id above two to the fifty-third loses its last digits to a float.
Sending anonymous_id alongside user_id on identify does not attach the earlier event to this profile. The identity link is written only by POST /v1/alias, and even that writes one link row: events already stored keep an empty user_id for ever. So a funnel that starts with an anonymous view and ends with a signed-in purchase will not join the two. Identity has the full picture and what can be done about it.
After this request a profile with the id u_123 exists. Before it there was none: an anonymous visitor gets no profile row at all.
The same call from JavaScript
Now that you have seen an event with your own eyes, the SDK.
The @segmentic/web package is not published on any registry and npm install @segmentic/web fails. What works today is the script tag, served from the same host the events go to, so your content security policy needs only one origin:
<script src="https://in.segmentic.net/sdk/segmentic.js"></script>
<script>
Segmentic.init({
writeKey: "wk_seg_...",
apiHost: "https://in.segmentic.net"
});
Segmentic.track("install_check", { source: "browser" });
</script>
Three things that otherwise look like a broken install:
- The SDK does not send immediately. It waits for twenty messages or ten seconds, whichever comes first.
await Segmentic.flush()empties the queue at once, but it posts toPOST /v1/batch, so the result does not show on the Live events page. Confirm arrival from the Check step on the Connect page. initsends a page view by itself, becauseautoPageViewdefaults to on.- If the browser has Do Not Track set, nothing is sent at all, because
respectDoNotTrackdefaults to on. This is the first thing to check when no events arrive.
The rest of the methods, the offline queue and browser push are in the web SDK.
The same call from a server
The same write key works from a backend. In Python:
import requests
response = requests.post(
"https://in.segmentic.net/v1/track",
headers={"Authorization": "Bearer wk_seg_..."},
json={
"message_id": "qs-3",
"event": "order_completed",
"user_id": "u_123",
"properties": {"revenue": 2500000, "currency": "IRR"},
},
timeout=10,
)
print(response.status_code, response.json())
The output:
200 {'status': 'ok', 'accepted': 1}
order_completed is one of the eleven standard names, and it is what the ready-made funnels and journeys are built on. Revenue is read from that property; failing that, total, then value, then price multiplied by quantity. The currency defaults to IRR when it is not stated, and no conversion is ever guessed.
For an event only your backend is certain of there is a second door: POST /v1/events on https://api.segmentic.net with an sk_seg_ key. That door differs in two ways and both are silent: it does not de-duplicate, so the same message_id sent twice becomes two events; and its window is a fixed thirty days, so anything older is clamped to the thirty-day edge and the warning about it is discarded. That is why history must not be migrated through it. The differences are in server to server.
What to read next
- Concepts, if you want to know exactly how a segment, an audience and a journey differ.
- Designing events, before you settle on event names. A wrong name is not fixed later.
- Placing events, to get from this one test event to the real events of your own site or app.
- Identity, if you have anonymous visitors who later sign in.
- The web SDK or the Android SDK for a real installation.
- Ingest endpoints for the rest of the routes: batching, device registration, the inbox.
- Errors and limits when you want the sending path to survive a bad day.