Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Calling plugins

This page describes how to call a plugin’s commands and queries over HTTP: headers, version checks, replay safety, the result query, and the error envelope. If you use an SDK, the SDK does most of this for you; still, knowing what happens makes debugging easier.

Note

For plugins there is no shared /openapi.json; the plugin’s own schema document (artifact) is obtained from the GET /v1/plugins/{plugin_id}/schema endpoint. The list of operations you can use is there (bindings).

Endpoints

Method and pathPurpose
POST /v1/plugins/{plugin_id}/commands/{message_type}Runs the command
GET /v1/plugins/{plugin_id}/commands/{message_type}/resultQueries the command’s result with the idempotency key
GET /v1/plugins/{plugin_id}/queries/{message_type}Runs the query (parameters in the query string)
POST /v1/plugins/{plugin_id}/queries/{message_type}Runs the query (parameters in the body)
GET /v1/plugins/{plugin_id}/schemaReturns the plugin’s schema document

{plugin_id} is the plugin ID (for example package-ride). {message_type} is the operation’s request message type (for example package-ride.progression-missions.login-bonus.input); it is not the operation’s name (claimLoginBonus). The full path and message type are written in the path and requestMessageType field of each operation in the schema document. Which method (GET or POST) to use is also stated by the operation’s method field: some server queries are called with POST.

Identity

CallerWhat to send
Player (cookie)Cookie (__Secure-crx.session) + x-crx-env: <environment uuid>; on modifying (POST) requests also x-csrf-token + the __Secure-crx.csrf cookie
Player (Bearer)Authorization: Bearer <session token> + x-crx-env: <environment uuid>. On modifying requests the plugin endpoint requires the CSRF pair (the __Secure-crx.csrf cookie + x-csrf-token) with Bearer too; a client without cookies gets the pair with GET /v1/auth/csrf and carries it itself (sessions and CSRF)
ServerAuthorization: Bearer crx_live_<prefix>_<secret> (a server key). If you send x-crx-env it must be the same as the key’s environment, otherwise 401. CSRF and Origin checks are not applied
Public queryOnly x-crx-env; only GET, and only queries that have access: public in the schema document

A key that starts with crx_ but is not of the server type (a client key) gets 401 unauthorized. In player requests, if there is an Origin header it must be one of the environment’s allowed origins (the browser adds it itself); otherwise 403 forbidden. When a modifying call succeeds, the response returns the new CSRF pair (Set-Cookie and the x-csrf-token header); use it on the next modifying request.

Calling a command

POST /v1/plugins/package-ride/commands/package-ride.progression-missions.login-bonus.input HTTP/1.1
Host: game.example.com
Origin: https://game.example.com
Cookie: __Secure-crx.session=<session>; __Secure-crx.csrf=<csrf-cookie>
x-crx-env: 0192a3b4-0000-7000-8000-000000000001
x-csrf-token: <csrf-token>
x-crx-schema-version: 1
Idempotency-Key: 5d1f2c88-4f0b-4c8f-9f1e-9a3f6b0c1d22
If-Match: "3"
Content-Type: application/json

{"expectedVersion":"3"}
HTTP/1.1 200 OK
Content-Type: application/json
x-crx-message-type: package-ride.progression-missions.login-bonus.output
x-crx-schema-version: 1

{"version":"4","rewardMinor":"250","streak":"2","day":"19300","walletBalanceMinor":"1250","claimEventId":"0192a3b4-0000-7000-8000-0000000000c1","ledgerCorrelationId":"0192a3b4-0000-7000-8000-0000000000c2"}
HeaderRequiredMeaning
Idempotency-Keyyes on commandsThe logical replay key; cannot be empty. Rules
x-crx-schema-versionyesThe schema version of the payload you send (a positive integer); must match the operation’s schemaVersion value. It is required on queries too
If-MatchnoThe expected resource version (optimistic lock). A decimal integer, quoted or unquoted; values with * and the W/ prefix get 400. If the real version differs, 409
Content-Type: application/jsonyesThe body is JSON and conforms to the operation’s request schema

Note

int64 values in body fields go as decimal text ("expectedVersion":"3"), not as a JSON number; the response has the same form. Details: numbers and encoding. int32 and uint32 fields are JSON numbers.

The body of a successful response is JSON that conforms to the operation’s response schema; the x-crx-message-type and x-crx-schema-version response headers indicate the output message.

Repeating with the same key

If the network drops or you cannot get the response, resend with the same Idempotency-Key and the same body; the server does not apply the operation a second time and returns the recorded result.

CaseResponse
First requestProcessed, 200
Same key, same bodyThe recorded result is returned
Same key, different body422 and idempotency_mismatch
A request with the same key is still being processed409 and idempotency_in_flight (retryable: true); try again shortly
The record expired (the operation’s receiptRetentionSeconds value, 86,400 s in the example)410 and idempotency_expired

Generating a new key means a new operation; do not resend a command with an uncertain outcome with a new key.

Result query

Find out the result of a command whose response you never received, without resending it:

GET /v1/plugins/package-ride/commands/package-ride.progression-missions.login-bonus.input/result?idempotency_key=5d1f2c88-4f0b-4c8f-9f1e-9a3f6b0c1d22&output_message_type=package-ride.progression-missions.login-bonus.output&output_schema_version=1 HTTP/1.1
x-crx-env: 0192a3b4-0000-7000-8000-000000000001
Cookie: __Secure-crx.session=<session>

All three query parameters are required: idempotency_key, output_message_type, and output_schema_version. The path is the same as the operation’s resultPath field.

ResponseMeaningWhat to do
200 + bodyThe command had been committed; the body is the outputUse the result
202 (empty body)Still being processedQuery again shortly
404There is no record with this key: the command was never appliedOnly in this case can you send the command with the same key and body
422 + reasonThe command was processed and rejected by a business ruleDo not send; reason is a machine code
410The record expiredThe result is unknown; re-read the state with a query

Calling a query

With GET, the parameters go in the query string. Each value is first parsed as JSON; if that fails, it is treated as text; the value null becomes null. Giving the same parameter twice gets 400.

GET /v1/plugins/package-ride/queries/package-ride.delivery-trip.active.input HTTP/1.1
x-crx-env: 0192a3b4-0000-7000-8000-000000000001
x-crx-schema-version: 1
Cookie: __Secure-crx.session=<session>

With POST, the same fields go in the JSON body. POST is suitable for complex or long inputs; read which method is valid from the operation’s method field. A public query example:

GET /v1/plugins/package-ride/queries/package-ride.world-catalog.get.input HTTP/1.1
x-crx-env: 0192a3b4-0000-7000-8000-000000000001
x-crx-schema-version: 1

Schema endpoint

GET /v1/plugins/{plugin_id}/schema returns the plugin’s schema document (JSON) (Cache-Control: no-store). A player session or a server key is required. If the plugin is not enabled for your environment or the grant version does not match, it is 404. You find the document’s fields and how to use the schemaSha256 value on the bindings page.

Error envelope

Plugin endpoints return a JSON error envelope (unlike the plain-text errors of the core REST endpoints; for a comparison see errors):

{"code":"conflict","message":"version conflict: expected 3, actual 5","retryable":false,"requestId":"0192a3b4-0000-7000-8000-0000000000d1"}

The response also has an x-request-id header; give this ID in a support request.

codeHTTPretryableWhen
unauthorized401noSession/key missing, invalid, a client key, or an environment mismatch
forbidden403noCSRF or Origin rejection; the plugin is not enabled or the version does not match; a banned account (message: account banned)
module_disabled404noThe plugins module is disabled (the module field carries the module name)
not_found404noUnknown plugin or message type; no record
invalid_argument400noMissing/broken header, unknown schema version, a payload that does not conform to the schema, a duplicate query parameter
conflict409noThe If-Match version does not match the real one
idempotency_in_flight409yesA request with the same key is still being processed
idempotency_mismatch422noSame key, different body
unprocessable422noA business rule rejection; the reason field carries the machine code
idempotency_expired410noThe record expired
rate_limited429yesRate limit; Retry-After: 60 (rate limits)
tenant_unavailable503yesThe environment’s placement is temporarily unavailable; Retry-After: 30
internal500yesServer error

The reason field exists only on a business rule rejection (unprocessable) (for example street_reset_too_early) and is plugin-specific; its meaning is in the plugin’s documentation. On a version mismatch (conflict) the message contains the expected and the real version: re-read the state and try again with the new version.

Warning

On plugin endpoints a banned account does not come with the account_banned code: 403 and code: "forbidden", message: "account banned" is returned. The account_banned code belongs to the player identity endpoints (sign-in, sign-up) (errors). If your client will distinguish these two cases, look at the session state, not at the message.

Example: calling a command safely against an uncertain outcome

The TypeScript example below prepares the command with an SDK binding (a single Idempotency-Key is generated), sends it; if the outcome is uncertain it queries the result without generating a new command and resends with the same key only on a notFound response. The demo-game plugin is a scenario prepared for this example: in your real plugin you take the operation names and fields from your own schema document (bindings).

import { CrxError, ErrorKind, type PlayerClient } from "@ferdiunal/open-play-ts";
import { DemoGameBinding } from "./generated/demo-game.js";

/**
 * Günlük hediyeyi alır. `prepare` tek `Idempotency-Key` üretir; ağ hatasında sonuç belirsizse yeni komut üretilmez:
 * önce sonuç sorgulanır, yalnız "kayıt yok" yanıtında AYNI anahtarla yeniden gönderilir.
 */
export async function claimDailyGift(client: PlayerClient, expectedVersion: bigint): Promise<bigint> {
  const demo = client.plugin(DemoGameBinding);
  const command = await demo.commands.claimDailyGift.prepare({ giftKey: "daily" }, { expectedVersion });
  try {
    const output = await command.send();
    return output.walletBalanceMinor; // int64 alanlar bigint gelir
  } catch (error) {
    if (!(error instanceof CrxError) || error.kind !== ErrorKind.UnknownOutcome) throw error;
    const outcome = await command.lookup();
    if (outcome.status === "committed") return outcome.result.walletBalanceMinor;
    if (outcome.status === "notFound") return (await command.resume()).walletBalanceMinor; // aynı anahtar, aynı gövde
    throw error; // pending, rejected ya da expired: kör tekrar yok
  }
}

/** Salt-okunur sorgu: tekrar güvenlidir, anahtar gerekmez. */
export async function hasClaimedToday(client: PlayerClient): Promise<boolean> {
  const demo = client.plugin(DemoGameBinding);
  const status = await demo.queries.getGiftStatus({ includeHistory: false });
  return status.claimedToday;
}

Next step