HTTP API
Anything that can make an HTTPS request can send events. Use this for orders confirmed after the shopper has left, refunds issued from an admin panel, or any system with no browser in it.
Base URL: https://edge.flofy.co
Endpoints
| Endpoint | Short form | type | For |
|---|---|---|---|
/v1/track | /v1/t | track | Something happened |
/v1/identify | /v1/i | identify | Who someone is |
/v1/page | /v1/p | page | A page view |
/v1/screen | /v1/s | screen | A screen view in an app |
/v1/group | /v1/g | group | Attach someone to a company |
/v1/alias | /v1/a | alias | Two IDs, one person |
/v1/batch | /v1/b | — | Several events at once |
All are POST with Content-Type: application/json. The short forms behave identically to the long ones.
Authentication
Your write key goes in an HTTP Basic header, as the username with an empty password. Base64-encode the key followed by a colon:
printf 'YOUR_WRITE_KEY:' | base64
Authorization: Basic <that value>
The trailing colon is required, and a non-empty password is rejected.
You can pass the key as a query parameter instead — ?writeKey=<base64 of the key> — which is what the browser SDK does, to keep requests free of a CORS preflight. When both are present, the query parameter wins.
A request
curl -X POST https://edge.flofy.co/v1/track \
-H "Authorization: Basic $(printf 'YOUR_WRITE_KEY:' | base64)" \
-H "Content-Type: application/json" \
-d '{
"type": "track",
"event": "Order Completed",
"userId": "customer_9931",
"messageId": "8f14e45f-ea0a-4c1f-9f2c-3b7d1a0e5c62",
"timestamp": "2026-08-25T10:31:00.000Z",
"properties": {
"order_id": "ORD-12345",
"currency": "SAR",
"value": 523.17,
"products": [
{ "product_id": "P-4471", "name": "Wireless Headphones", "price": 199.99, "quantity": 1 }
]
}
}'
The envelope
| Field | Required | Meaning |
|---|---|---|
type | Yes | Must match the endpoint |
event | On track | The event name |
messageId | Yes | Unique per event. A UUID is fine |
timestamp | Yes | When it happened — ISO 8601, UTC |
userId | One of the two | Your ID for a known person |
anonymousId | One of the two | An ID for someone not signed in |
properties | On track | The event's properties |
traits | On identify | What you know about the person |
context | No | Page, campaign, IP, user agent |
integrations | No | Turn individual destinations on or off |
messageId is how a retry is recognised as the same event rather than a second one. Generate it once per event and reuse it if you retry.
Envelope fields are camelCase — userId, messageId. Everything inside properties and traits is snake_case — order_id, first_name. The split is deliberate: the envelope describes the message, the properties describe your store.
Context
The SDK fills this in from the browser. Over HTTP you send what you have, and all of it is optional:
{
"context": {
"ip": "203.0.113.9",
"userAgent": "Mozilla/5.0 …",
"locale": "ar-SA",
"timezone": "Asia/Riyadh",
"page": {
"url": "https://store.example.com/checkout",
"path": "/checkout",
"referrer": "https://www.google.com/"
},
"campaign": {
"source": "facebook",
"medium": "cpc",
"name": "ramadan-2026"
}
}
}
ip and userAgent are the two worth the effort. Meta, TikTok and Snapchat use them to match a server-side conversion to an ad view, and without them a share of your orders goes unattributed. Send the shopper's address, not your server's.
Batching
{
"batch": [
{
"type": "identify",
"userId": "customer_9931",
"messageId": "…",
"timestamp": "…",
"traits": { "email": "sara@example.com" }
},
{
"type": "track",
"event": "Order Completed",
"userId": "customer_9931",
"messageId": "…",
"timestamp": "…",
"properties": { "order_id": "ORD-12345", "currency": "SAR", "value": 523.17 }
}
]
}
Every element is a complete event with its own type, messageId and timestamp. Elements are validated before any of them is accepted, so a single malformed entry rejects the whole request — which is what makes the request safe to retry as a unit.
Limits
| Single event | 256 KB |
| Batch request | 5 MB |
| Events per batch | 1,000 |
Responses
| Code | Meaning |
|---|---|
204 | Received |
400 | Missing or malformed write key, invalid JSON, or a bad batch element |
413 | More than 1,000 events in one batch |
503 | Flowfy could not accept the event — retry with backoff |
There is no response body on success. A 204 means the request was well-formed, not that the write key matches a source: a valid-looking key for a source that does not exist is accepted here and dropped later. Confirm in Live events on the source's page in Flowfy.
When something is wrong
Work down this list:
- Is the header right?
Basic, a space, then base64 ofkey:— with the colon. - Does
typematch the endpoint? Atrackbody posted to/v1/identifywill not do what you meant. - Is identity in the body?
userIdoranonymousIdhas to be in the JSON. A header does not count. - Is
timestampISO 8601 in UTC?2026-08-25T10:31:00.000Z, not a Unix epoch. - Are the properties snake_case?
order_id, notorderId. The wrong casing is accepted and then ignored, which looks like a delivery problem but is not. - Does the event name match the reference? Check the event reference.