> For the complete documentation index, see [llms.txt](https://sdk.metamarket.tech/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://sdk.metamarket.tech/evaluate-and-integrate/api-guide.md).

# HTTP API

This page documents the raw HTTP API for integrators who work without the client SDK. It covers: [the headers every call needs](#headers), [the response envelope](#the-response-envelope), [which endpoint to call](#choosing-the-endpoint-track-or-execute), [how to address one specific quest, package, or trade](#how-a-client-points-at-one-specific-quest-package-or-trade), and [the error-code reference](#errors). For the typed client see [SDK Guide](/evaluate-and-integrate/sdk-guide.md).

An external game reaches this surface through the edge gateway, which forwards every call to the game engine. The gateway treats three kinds of call differently on the way:

| Call          | Routes                                            | What the gateway does                                                                      |
| ------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Player-facing | `/api/slots/*`, `/api/player/*`, `/api/session/*` | Passes your player JWT through unchanged                                                   |
| Event         | `/api/events/*`                                   | Attaches your app credential, which never reaches your client                              |
| Auth          | `/api/auth/*`                                     | Forwards anonymously. The engine then proxies to the Hub under its own platform credential |

Identity lives in the Hub, but the engine sits on the authentication path. This page documents that external surface.

## Headers

Two headers matter across almost every call:

* `X-App-Id`: required on every call except `/api/auth/*`. Identifies which tenant's config and data the request runs against. Missing header → `400`. The gateway rejects an `X-App-Id` it is not configured to serve, and selects the app secret only from its own server-side configuration, never from the client. On event calls the engine takes the acting app id from this header and overwrites anything the request body claims. What the client never holds is the secret: the gateway validated the header and the secret as a bound pair before forwarding. A raw HTTP client is bound by the same control: the gateway in front of an external game is provisioned for one app id, holds that app's secret server-side, and rejects any other `X-App-Id`, so the only app id a caller can act as is that one provisioned app, whether it uses the SDK or plain HTTP. See [Security and Tenant Isolation](/architecture-and-trust/security-and-tenant-isolation.md).
* `X-Virtual-Time-Offset`: optional, seconds to add to server time for the request scope. **Development-only**: the server ignores the header entirely outside a development environment. Quest lifecycle timing tests rely on it heavily.

For an external game the gateway handles app authorization; your client carries the player JWT (`Authorization: Bearer <token>`). The full model is in [Authentication](/evaluate-and-integrate/authentication.md).

## The response envelope

Every mutating domain call returns the same shape, `ClientResponse<T>` (authentication calls return their own token shapes, and read-only queries return their result directly):

```json
{
  "data": { "...": "endpoint-specific result" },
  "directives": [],
  "effects": [],
  "mutations": []
}
```

`data` is the domain result for that endpoint (a batch count, a receipt, a pool item state). The three side-channels are optional; they stay empty when nothing relevant happened:

* `effects`: echoed scalar values the operation touched, each an entry `{ entityId, key, kind, value }` where `kind` is `absolute` (the value is the new total, including derived resources like mood) or `delta` (add it to the client's current value), so a client can update a resource bar without a follow-up `GET /api/player/state` read.
* `mutations`: full DTO-entity states touched by the operation, one polymorphic channel across domains (a pool item, a bundle, …), keyed for dedup on the client side.
* `directives`: imperative instructions (e.g. a reveal card) the client must act on.

`data` answers "what happened"; the three side channels carry state the client should apply. Apply them in a fixed order: fold `effects`, then `mutations`, then run the `directives`. The order matters, because a directive can say "this content changed, re-fetch it", and running it after the fold stops a stale mutation re-adding what the refresh just removed. Once all three are applied the envelope has already carried the change, so a routine follow-up read is redundant; [Reading State](/evaluate-and-integrate/reading-state.md#reading-versus-echoing) covers the cases that still need one. For the full channel semantics, and for how the .NET SDK consumes them, see [Sending Actions](/evaluate-and-integrate/events-and-directives.md#reading-the-response).

One exception to that. The envelope carries what the call changed for the acting player. A marketplace sale also credits the seller and moves the item to the buyer server-side, and those changes are not in your envelope, so each party reads their own state for them.

This envelope is the response of a *mutating* call. Read-only queries such as `GET /api/player/state` return their result directly, with no side channels.

## Choosing the endpoint: track or execute

Whether the caller needs the result right away is what picks the endpoint. `/api/events/track` and `/api/events/execute` live under the same `/api/events` group and take the same request shape: a JSON array of actions (`playerId`, `actionType`, `value`, `clientKey`, `timestamp`, optional `entityId`). They differ in what the caller gets back and what kind of action they accept:

|                 | `/api/events/track`                                                                                                          | `/api/events/execute`                                                                                                              |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Semantics       | Fire-and-forget (no domain receipt)                                                                                          | Synchronous (returns a receipt)                                                                                                    |
| Response `data` | Batch result (`processed`/`skipped` counts)                                                                                  | Execute result with a `receipts[]` array                                                                                           |
| Routing         | Runs against your game's engine; any milestone the platform observes is handled by a platform-owned quest, not returned here | Runs against your game's engine only                                                                                               |
| Pick this when  | The action's config sets `output.require_receipt: false` (default), such as `open_app`, `tap`, `nurse`                       | The action's config sets `output.require_receipt: true`, because the client needs to know what happened right away, e.g. `do_spin` |

The gate is bidirectional. An action configured `require_receipt: true` that reaches `/track`, or a `false` one that reaches `/execute`, is not processed. The call still returns `200`, the action is counted in `skipped` rather than `processed`, and a warning is logged server-side. Nothing errors, so a client that ignores the count does not notice. Detecting that and recovering from it is in [Sending Actions](/evaluate-and-integrate/events-and-directives.md#treat-a-non-zero-skipped-as-a-failure).

### What the event endpoints authenticate

The authorization on these endpoints authenticates the *caller*, not the end player. The gateway attaches the app credential on the game's behalf, which is why an external integration always goes through it rather than calling the engine directly.

The `playerId` in the action body names which player the action is for. The engine does not compare it against the subject of the player token on the call. Authorization establishes which app is reporting, and the app is trusted to name its own players correctly within its own scope.

That leaves a responsibility with your server-side integration: do not accept an unvalidated player id from an untrusted client and pass it straight through.

## How a client points at one specific quest, package, or trade

Quests, packages, and trades are all pool items, addressed by the stable pair `(slotId, itemSlug)` rather than by a transient instance id. A slot is a socket and an item slug is the stable identity of what is plugged into it, so re-baking a fresh incarnation of the same content does not change the address a client already bookmarked.

**Discovery (GET, read-only):**

* `GET /api/player/items`: every pool item the player has across all slots for the current `X-App-Id`. Return-all semantics: exhausted or terminal items are included too, and the client decides what to grey out.
* `GET /api/slots/{slotId}/items`: same return-all discovery, scoped to one slot.
* `GET /api/slots/{slotId}/items/{itemSlug}`: single item state by address, or `404`.

All three accept flat query-string filters from `PoolFilterKeys`:

* `contentType` (`quest`/`package`/`trade`/`listing`) is filtered centrally. `trade` is a studio-defined shop product (priced in `costs`, no seller); `listing` is a player-created marketplace listing (carries `sellerPlayerId`, priced in `price`).
* Type-specific keys are read by each content strategy itself: `questStatus` for quests, `hasCharges` for packages.
* The response body is polymorphic on a `contentType` JSON discriminator (`QuestStateContract` / `PackageItemStateContract` / `TradeItemStateContract` / `ListingItemStateContract`).
* All variants share common fields:
  * `slotId`
  * `slotDefId`
  * `itemSlug`
  * `entityId`
  * a `bundle` quota block (current/max charges, regen type, regen anchor)

The three mutating pool verbs accept an optional JSON body carrying an idempotency key, `{ "clientKey": "..." }`. Send one if you intend to retry safely; without it a repeat is a second application. The typed client takes it as the optional last argument, for example `UseAsync(slotId, itemSlug, clientKey)`.

**Mutation (POST, one universal verb + two quest-only verbs):**

* `POST /api/slots/{slotId}/items/{itemSlug}/use`: the universal, quota-gated primary verb. It decrements one bundle charge and triggers the content's primary effect: for a quest this is accept (`available` → `active`); for a package this is grant (execute the baked reward payload into the pool entity); for a trade this is purchase. Returns `400` with a `business_logic` code if the bundle is exhausted, `403` on an app-scope violation the caller doesn't own.
* `POST /api/slots/{slotId}/items/{itemSlug}/evaluate`: quest-only. Re-runs quest evaluation (progress check, JIT expiry, auto-renew) without consuming a charge.
* `POST /api/slots/{slotId}/items/{itemSlug}/tasks/{taskSlug}/claim`: quest-only. Claims a task's reward once it is `ready_to_claim`, then runs orchestration for the next stage.

Every mutation returns the same `ClientResponse<PoolItemStateContract>` envelope described above: `data` is the mutated item's new state, `effects`/`mutations` carry any touched scales or cascaded entities (e.g. a `feed` package changing hunger and mood together), and `directives` carry a reveal card if the action's config asks for one.

## Reading player state

* `GET /api/player/state`: the player's composite state for the current `X-App-Id`, returned as the domain result directly with no envelope (there is nothing to act on). Values that regenerate or decay are settled to the moment of the read.
* `GET /api/player/inventory`: boxes and items the player holds, including unopened packages.
* `POST /api/player/inventory/{inventoryId}/open`: opens a stored package; returns the standard envelope with the fulfilled contents.
* `GET /api/player/local-stats`: the player's cumulative per-action stat totals for this app.

[Reading State](/evaluate-and-integrate/reading-state.md) explains when to make a read call and when the `effects` and `mutations` an action already returned.

## Session, config, and bundles

The other routes an external integration uses through the gateway:

* `POST /api/session/start`: begins a session and returns the app's `start_session_directives` for the client to run (see [App Configuration](/evaluate-and-integrate/app-configuration.md)).
* `GET /api/apps/config`: the app's own configuration contract.
* `GET /api/bundles`, `GET /api/bundles/{instanceId}`, `POST /api/bundles/{instanceId}/use?clientKey=...` (the `clientKey` query parameter is required; without it the call is rejected with `400`): the regenerating-charge bundles behind refillable quotas.

These are the same routes the SDK's session, app, and bundle clients call; any non-.NET stack uses them directly over HTTP.

## Errors

Business-rule rejections come back as `400` `ProblemDetails` with a stable `code` extension, not a `500`; anything unhandled falls through to a generic `500` with no internal detail exposed. The raw HTTP response carries that `code`, so a client reading the API directly can branch on it instead of parsing an error string. The .NET SDK today surfaces the `400` status and the message. A `Game.Sdk` call returns an `ApiResponse<T>` whose `IsSuccess` is false on a business rejection, carrying the status and message, so an SDK client branches on `IsSuccess` and the status rather than catching a thrown exception (see [SDK Guide](/evaluate-and-integrate/sdk-guide.md)).

| `code`                                   | When you'll see it                                                                                                                                           |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `target_entity_required`                 | A `scope: target` action was sent without `entityId`.                                                                                                        |
| `business_logic`                         | A business-rule rejection with no more specific code: bundle exhausted, item not found, or a value that would drive a resource below its floor (Zero-Floor). |
| Integrity Violation (`business_logic`)   | A written key is not registered as a `stat`-type action in `allowedActions` at all.                                                                          |
| Strict Mode, stat (`business_logic`)     | The key is registered as a `stat` but has no declared scope.                                                                                                 |
| Strict Mode, resource (`business_logic`) | A resource write's key is not declared with a scope in `mechanics.resources`.                                                                                |

A business rejection raised while an action is executing on `/api/events/track` or `/api/events/execute` does not reach this table. The action processor catches it per action, so the call returns `200` with that action counted in `skipped` and no error body at all. The codes in the table above apply to the direct pool, quest and bundle verbs, and `target_entity_required` is also returned by the event endpoints.

Two further outcomes are deliberately not `400`:

* **An app-scope or ownership violation** on a pool-item verb (`/use` and friends) returns `403`. A raw player JWT sent straight to the engine on an event call is also refused with `403` when the app id names an external app, which is every app an external integration is provisioned for. First-party apps are admitted on a different path an external integration never uses.
* **An action verb not declared in `allowedActions`** is not rejected at all. It is not processed and is counted in the response's `skipped` on an HTTP 200, which is an integration failure to treat as such. See [Sending Actions](/evaluate-and-integrate/events-and-directives.md#treat-a-non-zero-skipped-as-a-failure).

The declare-before-write `400`s above apply to a declared action whose stat or resource key is itself undeclared. This list is not exhaustive. Branch on `code`, and treat an unrecognised `code` on a `400` as a business rejection.

***

Last reviewed 2026-07. Documentation release `docs-release-2026-07`; runtime `net10.0`. The HTTP API carries no route-level version and the SDK and schema are separately unversioned ([Compatibility](/releases-and-reference/compatibility.md)). Status source of truth: [Product availability](/overview/current-scope.md).
