Building a mission

This guide builds one mission end to end: 250 points and a catalog item when a player finishes five lessons.

It takes both halves of the product. You define and author in the dashboard, then run against the Public API. No endpoint creates an event, a catalog item or a mission, and none reads a definition back, so the first two steps are dashboard work.

Step 1: define what you count and what you pay

In the dashboard, with the right project and environment selected (Sandbox while you build):

  1. Events. Create an event with key lesson_completed. The key is lowercase snake_case and is what you will name when you send it.
  2. Catalog. Create a catalog item with key bonus_lesson_pack and value type Number. A catalog item is a name for something your own system hands out. Gemifier never hands it over, it only asks you to.

Keys belong to an environment. A key that exists in Sandbox does not exist in Live until you create it there too, and an API key writes only to its own environment.

Step 2: author the mission

On Missions, create a mission with key finish_five_lessons, one objective and two rewards. The objective is a rule: lesson_completed happened at least 5 times. No endpoint accepts a rule, so the dashboard is the only place it is set, and it reaches your integration only in reverse, as the progress readings in Step 5.

The settings that matter for the rest of this page:

SettingValue hereEffect
Opt-inAutomaticThe player takes part as soon as the mission is returned to them. Manual needs an accept call.
Objective orderAny orderObjectives can be met in any order. In order requires each one's predecessors first.
Where it appearsThe feedIt shows in the player's missions. Collection only hides it from the feed.
RepeatsNoA one-time mission. Its occurrenceNumber is always 0.
Who can take partEveryoneRestrict it, and you choose whether a player who does not qualify sees it locked or not at all.

A mission needs at least one objective and at least one reward. Its key, objectives, order, opt-in and recurrence are fixed once it is created, so changing an objective means creating a new mission and switching the old one off.

Give it two rewards: 250 points, and 3 of bonus_lesson_pack. The two settle differently, and that difference shapes everything below. Points are internal and Gemifier moves the balance itself. A catalog item is external: Gemifier records that it is owed and asks you for it over a webhook. A reward is copied when it is granted, so editing the mission later does not rewrite what a player already earned.

Step 3: enter the player

POST /v1/missions returns the player's missions, and returning one to a player is what enters them into it. It creates the player if they are new, and starts every mission they newly qualify for.

Read a player's missions
curl -X POST https://api.gemifier.io/v1/missions \
  -H "Authorization: Bearer gem_sbox_..." \
  -H "Content-Type: application/json" \
  -d '{
    "playerId": "player-42",
    "includeCompleted": false,
    "languageCode": "en"
  }'

This is the step most integrations miss. An event sent for a player whose missions have never been read advances nothing, and no error says so. Call this when a player first appears, and again whenever you show them their missions.

On this API a POST means something changed, which is why the mission reads are POST while the streak and shop reads are GET. Do not treat it as a safe read to hammer in a loop.

The body takes playerId (required), take to cap how many come back, includeCompleted (false by default) and languageCode.

Step 4: send the events

One finished lesson
curl -X POST https://api.gemifier.io/v1/events \
  -H "Authorization: Bearer gem_sbox_..." \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      {
        "playerId": "player-42",
        "event": "lesson_completed",
        "occurredAt": "2026-08-20T09:14:22Z",
        "idempotencyKey": "lesson-88f1c0"
      }
    ]
  }'

An event moves only the missions whose objectives name that key. A mission built on deposit_made does not move on a lesson_completed event, and a key that appears only in who can take part wakes nothing, because that is settled when the missions are read. See Sending events for the full field list.

If opt-in is Manual, the mission sits at Available and does not advance until the player accepts:

Accept a mission
curl -X POST https://api.gemifier.io/v1/missions/finish_five_lessons/accept \
  -H "Authorization: Bearer gem_sbox_..." \
  -H "Content-Type: application/json" \
  -d '{ "playerId": "player-42" }'

That endpoint returns 204 with no body. Accepting twice does nothing, and accepting an automatic mission is harmless. Accepting one the player was never offered is 404 mission.not_locked_in. Nothing undoes an accept.

Step 5: read the progress back

Call POST /v1/missions again.

200 OK, three of five lessons in
[
  {
    "missionKey": "finish_five_lessons",
    "name": "Finish five lessons",
    "description": "Finish five lessons to earn a bonus pack.",
    "images": {},
    "status": "InProgress",
    "optInMode": "Automatic",
    "isAccepted": true,
    "occurrenceNumber": 0,
    "objectives": [
      {
        "objectiveId": "0198f1c4-31aa-7b02-8f4c-1d9e5a7c3b60",
        "description": "Finish five lessons",
        "order": 0,
        "isSatisfied": false,
        "progress": {
          "kind": "TowardTarget",
          "actual": { "type": "number", "value": 3 },
          "operator": "GreaterThanOrEqualTo",
          "target": { "type": "number", "value": 5 },
          "fraction": 0.6
        }
      }
    ],
    "rewards": [
      { "kind": "Points", "pointsAmount": 250, "catalogItemKey": null, "catalogItemValue": null },
      {
        "kind": "CatalogItem",
        "pointsAmount": null,
        "catalogItemKey": "bonus_lesson_pack",
        "catalogItemValue": { "type": "number", "value": 3 }
      }
    ],
    "startedAtUtc": "2026-08-20T09:02:10.442Z",
    "acceptedAtUtc": "2026-08-20T09:02:10.442Z",
    "completedAtUtc": null,
    "availableUntilUtc": null
  }
]

status is one of Available, InProgress, Completed or Locked. Available is a manual mission the player has not accepted. Locked is the only one they are not in: startedAtUtc is null, no objective is satisfied, and every objective reports NotDisplayable.

The progress shape

Every objective carries a progress object with these five fields, and it is never null. An objective with nothing to show reports kind: "NotDisplayable" rather than leaving it out, so a client can never read "missing" as "zero".

FieldTypeMeaning
kindenumTowardTarget, WithinLimit, Binary or NotDisplayable. Switch on this, not on the shape of the values.
actualvalue or nullWhere the player is. null when there is no reading, which is not the same as zero.
operatorenum or nullWhat the reading is measured by.
targetvalue or nullWhat it is measured against.
fractionnumber or nullA ratio from 0 to 1, only for TowardTarget and WithinLimit, and only when both values are known. Its absence is the signal not to draw a bar.

actual and target are tagged: a type of string, number, boolean, dateTime or collection, with value on the first four and items on a collection.

What each kind means on screen:

  • TowardTarget counts up to a target. A filling bar is right here and only here.
  • WithinLimit stays under a ceiling. It starts satisfied and is lost as the value rises, so fraction: 1 means gone rather than done. Draw a draining budget.
  • Binary is a yes or a no. fraction is always null.
  • NotDisplayable has no single reading to show. Only isSatisfied means anything.

isSatisfied is the authority, not progress. A satisfied objective stays satisfied even if the value behind it falls back, so the two can disagree: a WithinLimit objective the player has since broken reads isSatisfied: true with fraction: 1.

order and occurrenceNumber both count from 0. Add one before showing a number to a person.

Step 6: receive the completion

mission.completed reports the fact. It is sent only if an endpoint in that environment subscribes to it.

POST to your endpoint, X-Gemifier-Event: mission.completed
{
  "version": 1,
  "event": "mission.completed",
  "deliveryId": "0198f1c5-77ab-7d19-95e2-4c0b8e6f2a13",
  "occurredAtUtc": "2026-08-20T09:31:07.118Z",
  "data": {
    "playerExternalId": "player-42",
    "missionKey": "finish_five_lessons",
    "occurrenceId": "0198f1c5-77ab-7d19-95e2-4c0b8e6f2a13",
    "occurrenceNumber": 0,
    "completedAtUtc": "2026-08-20T09:31:07.118Z"
  }
}

catalog_item.fulfillment_requested is the instruction. Its sourceId is the same value as the completion's occurrenceId, which is how you join the two:

POST to your endpoint, X-Gemifier-Event: catalog_item.fulfillment_requested
{
  "version": 1,
  "event": "catalog_item.fulfillment_requested",
  "deliveryId": "0198f1c5-7a02-7e44-b6d1-8f2c0a5b93e7",
  "occurredAtUtc": "2026-08-20T09:31:07.118Z",
  "data": {
    "playerExternalId": "player-42",
    "catalogItemKey": "bonus_lesson_pack",
    "valueType": "Number",
    "value": 3,
    "sourceType": "Mission",
    "sourceId": "0198f1c5-77ab-7d19-95e2-4c0b8e6f2a13"
  }
}

A notification carries no amounts. mission.completed never names the points or the catalog item, so there is nothing on it to credit. One completion paying three catalog items produces three instructions sharing one sourceId, and one paying only points produces none. See Events.

What to poll, and what to wait for

Sending is immediate. Everything worked out from it is not, and nothing reports how far behind it is. Usually it is seconds.

Poll the missions for progress and status. That read and the collection read are the only public reads of mission state. Poll when a screen is opened, not in a tight loop: both of them write.

Read the balance with GET /v1/points. Mission points land at the same moment the completion does, so any read that sees status: "Completed" sees the new balance too.

Wait for the webhook for a catalog item. There is nothing to poll: nothing reports whether a delivery was made, retried or given up on. If no endpoint is subscribed, nothing tells you the item was earned.

Do not poll for eligibility. A player becomes eligible the next time you call POST /v1/missions for them, and not before.

Traps worth knowing before you go live

  • A manual mission that is never accepted never advances, and shows as Available for good.
  • includeCompleted is false by default, so a finished mission disappears unless you ask for it.
  • take caps how many come back. Locked ones come after everything the player can act on.
  • A closed window stops new players joining, not the ones already in. A mission whose window has closed is never offered to somebody new, and is not shown as locked either. A player who joined before it closed still sees their row, with availableUntilUtc in the past.
  • An event cannot be switched off while an active mission or streak uses it. The dashboard refuses and names what depends on it, so switch the mission off first.
  • Sandbox and Live share nothing below the environment. Authoring a mission in Sandbox does not create it in Live, and a gem_sbox_ key cannot see a Live mission.

Next

On this page