One POST to send activity in. JSON endpoints to read it back out.
Your backend sends one authenticated HTTPS request when something happens. The same backend reads missions, streaks, achievements, leaderboards, points and the shop back out. No SDK, no agent, nothing inbound to your network.
Four steps from an empty account to a mission that completes.
Mint a key and declare what you want to track in the dashboard, then send an event and read the result from your code. The last two steps are a single POST each.
Get your Sandbox keys
Every project has two environments, Sandbox and Live. Mint a key inside Sandbox. It is shown once, because we keep only a SHA-256 hash. If you lose it, revoke it and mint another.
Declare an event
Declare what you want to track: a key such as purchase, and any
properties to filter or sum on. An undeclared key is refused at ingest, unless
discovery is switched on.
Send your first event
One POST, one header. The first event carrying an unknown
playerId creates that customer.
Read the result back
Build a mission that uses the event and send it again. Fetch the result with
POST /v1/missions. That response is the shape your front end renders.
- An outbound HTTPS client. Whatever your stack already uses.
- One header.
Authorization: Bearer gem_sbox_… - Your own id for the customer. We never issue one.
- Nothing inbound. No SDK, no agent.
Two write endpoints: one for things that happen, one for facts about the person.
POST /v1/events records a batch of things that happened, such as a purchase.
PUT /v1/attributes records facts about the person right now, such as their
tier, and the last write wins. Both create the customer on first contact.
- Batch whenever you can. A thousand events cost little more than one call: authenticating the key is most of the cost.
- Two timestamps. You send
occurredAt. We record arrival separately, so a replayed backlog lands in the right day. - Properties are validated against what you declared, unless discovery is switched on, in which case an unknown key defines itself.
- A 200 doesn't mean every item was accepted. Check
resultsfor which succeeded. - The customer is created on first contact. No registration call.
{ // required: at least one, at most 1,000 per call "events": [ { // required: your own id for the end user, never ours "playerId": "cust_8f2a41c", // required: an event key you declared, snake_case "event": "purchase", // required: when it happened, not when you sent it "occurredAt": "2026-07-28T18:24:05Z", // optional: makes a retry replay instead of duplicate "idempotencyKey": "ord_9f31c2", // optional: typed, and filterable or summable inside conditions "properties": { "amount": 64.90, "method": "card", "channel": "mobile-app" }, // optional: IANA zone, used for day boundaries "playerTimeZoneId": "Europe/Belgrade" } ] }
curl -X POST https://api.gemifier.io/v1/events \ -H "Authorization: Bearer $GEMIFIER_KEY" \ -H "Content-Type: application/json" \ -d '{ "events": [{ "playerId": "cust_8f2a41c", "event": "purchase", "occurredAt": "2026-07-28T18:24:05Z", "idempotencyKey": "ord_9f31c2", "properties": { "amount": 64.90, "method": "card" } }] }' # the key decides Sandbox or Live. Swap the environment # variable, not the URL and not the body.
{ "accepted": 1, "rejected": 0, "results": [ { "index": 0, "accepted": true, "eventId": "0198f4c2-91a7-7c40-8b12-6d0e3f5a2c19", "errors": [] } ] } // one entry per event you sent, in the same order. // a retry carrying the same idempotency key replays // the same eventId instead of creating a second one. // // evaluation happens after the response, and cannot // be lost once you are holding these ids.
// facts about the person, not things they did { "playerId": "cust_8f2a41c", // one or more attribute keys you declared, in one call "attributes": { "tier": "gold", "country": "DE" }, "occurredAt": "2026-07-28T18:24:05Z" } // last write wins: an attribute is a current state, // not a running total, and eligibility rules read it. // all or nothing: an unknown key or a wrong type // refuses the whole request and writes none of it.
| Field | Type | Required | Notes |
|---|---|---|---|
| events | array | yes | At least one, up to 1,000 per call. A larger batch is refused whole. |
| playerId | string | yes | Your own id for the customer, up to 200 characters. An unknown value creates it. |
| event | string | yes | An event you declared, snake_case, active in this environment. |
| occurredAt | timestamp | yes | When it happened, not when you sent it. An absolute instant: include an offset or a trailing Z. |
| idempotencyKey | string | no | Unique per event. A retry replays the original rather than duplicating it. |
| properties | object | no | Checked against the event's definitions: String, Number, Boolean or DateTime. |
| playerTimeZoneId | string | no | An IANA zone such as Europe/Belgrade. It sets day boundaries. |
The endpoints behind a customer's progress screen.
Every POST changes something, and the GETs only read. A progress screen gets everything it needs on the same key you write with: the mission feed with progress per objective, the current and best streak, every achievement held or not, their rank, the points balance and ledger, and the shop.
| Endpoint | What comes back |
|---|---|
| POST /v1/missions | The mission feed: objectives, rewards and status. |
| POST /v1/mission-collections/{collectionKey} | One collection, in the order you curated it. |
| POST /v1/missions/{missionKey}/accept | Opts a customer into a mission that needs it. |
| GET /v1/streaks | The live run, the longest run, and when it ends. |
| GET /v1/achievements | Every achievement, held or not, each with its own progress. |
| GET /v1/achievements/{achievementKey} | One achievement. Unknown, switched off and hidden all read as 404. |
| GET /v1/leaderboards/{key} | Top, Around or Named entries, with rank and score. |
| GET /v1/points | The current balance. An unknown customer has zero, not a 404. |
| GET /v1/points/transactions | The ledger, newest first, with the cause of each move. |
| POST /v1/points/adjustments | A manual credit or debit, with a reason recorded. |
| GET /v1/marketplace-items | What this customer can see and buy right now. |
| GET /v1/marketplace-collections/{collectionKey} | One curated shop section. |
| POST /v1/marketplace-items/{key}/purchases | Buys one unit, spends the points and records a grant. |
A purchase passes six checks, in order. It stops at the first failure, so the error code tells you which one closed.
- Ingest, adjustments and purchases take an idempotency key. A retry returns the original. Nothing is counted twice.
- One error shape. RFC 9457
problem+json, with acodeyou switch on, every problem listed at once, and atraceId. - You issue what the customer receives. We record the grant against your catalog item. Money never touches us.
One key belongs to one environment for its whole life.
The key you send decides whether you reach Sandbox or Live, and nothing else does. A test key never reaches a real customer.
- Prefixed by environment.
gem_live_…orgem_sbox_…, so a key identifies itself. - Shown once, revocable at any moment. We keep a SHA-256 hash.
- People sign in separately. Servers hold a project key. Your colleagues use the dashboard. A leaked project key cannot edit a mission.
- Webhooks are opt-in. Register one or more HTTPS endpoints; we POST catalog grants, mission completions, streak milestones and achievements to every endpoint that wants them, signed with HMAC-SHA256. Delivery repeats, so de-duplicate on
deliveryId.
One event per request. Tell us early if your volume is high.
You get JSON, not a progress bar. Building the screen is real front-end work.
Each API serves an OpenAPI document. Generating a typed client is your build step.
Mission funnel, points in and out, active customers, streak outcomes and marketplace sales are all real, requiring sign-in. A project key reads one customer at a time.
Send one event and watch what it moves.
Bring the name of one thing you track and one campaign you want to launch. We will wire it up in a test account and complete it live on the call.