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 theGET /v1/plugins/{plugin_id}/schemaendpoint. The list of operations you can use is there (bindings).
Endpoints
| Method and path | Purpose |
|---|---|
POST /v1/plugins/{plugin_id}/commands/{message_type} | Runs the command |
GET /v1/plugins/{plugin_id}/commands/{message_type}/result | Queries 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}/schema | Returns 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
| Caller | What 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) |
| Server | Authorization: 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 query | Only 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"}
| Header | Required | Meaning |
|---|---|---|
Idempotency-Key | yes on commands | The logical replay key; cannot be empty. Rules |
x-crx-schema-version | yes | The schema version of the payload you send (a positive integer); must match the operation’s schemaVersion value. It is required on queries too |
If-Match | no | The 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/json | yes | The body is JSON and conforms to the operation’s request schema |
Note
int64values in body fields go as decimal text ("expectedVersion":"3"), not as a JSON number; the response has the same form. Details: numbers and encoding.int32anduint32fields 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.
| Case | Response |
|---|---|
| First request | Processed, 200 |
| Same key, same body | The recorded result is returned |
| Same key, different body | 422 and idempotency_mismatch |
| A request with the same key is still being processed | 409 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.
| Response | Meaning | What to do |
|---|---|---|
200 + body | The command had been committed; the body is the output | Use the result |
202 (empty body) | Still being processed | Query again shortly |
404 | There is no record with this key: the command was never applied | Only in this case can you send the command with the same key and body |
422 + reason | The command was processed and rejected by a business rule | Do not send; reason is a machine code |
410 | The record expired | The 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.
code | HTTP | retryable | When |
|---|---|---|---|
unauthorized | 401 | no | Session/key missing, invalid, a client key, or an environment mismatch |
forbidden | 403 | no | CSRF or Origin rejection; the plugin is not enabled or the version does not match; a banned account (message: account banned) |
module_disabled | 404 | no | The plugins module is disabled (the module field carries the module name) |
not_found | 404 | no | Unknown plugin or message type; no record |
invalid_argument | 400 | no | Missing/broken header, unknown schema version, a payload that does not conform to the schema, a duplicate query parameter |
conflict | 409 | no | The If-Match version does not match the real one |
idempotency_in_flight | 409 | yes | A request with the same key is still being processed |
idempotency_mismatch | 422 | no | Same key, different body |
unprocessable | 422 | no | A business rule rejection; the reason field carries the machine code |
idempotency_expired | 410 | no | The record expired |
rate_limited | 429 | yes | Rate limit; Retry-After: 60 (rate limits) |
tenant_unavailable | 503 | yes | The environment’s placement is temporarily unavailable; Retry-After: 30 |
internal | 500 | yes | Server 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_bannedcode:403andcode: "forbidden",message: "account banned"is returned. Theaccount_bannedcode 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
- Typed client and schema pinning: bindings.
- Rules for replay safety and version checks: idempotency and concurrency.