Enjin
Enjin Platform V3 Graph Book
A practical, growing reference of verified Enjin Platform V3 GraphQL queries, mutations, variables, responses and implementation quirks.
Graph Book Status
Work in progress, started 19 August 2026. Entries will be added as each Platform V3 query or mutation is used and verified.
This is the Platform V3 successor to the original Kepithor Enjin Graph Book. It is intended as a practical notebook for developers: working operations, realistic variables, redacted responses and the quirks discovered while integrating them.
Wallet daemon installation and V2 migration are documented separately in the Enjin Platform V3 Wallet Daemon Upgrade Guide.
Production Endpoint and Authentication
| Purpose | URL |
|---|---|
| Application GraphQL queries and mutations | https://platform.enjin.io/graphql |
| Wallet daemon communication | https://platform.enjin.io/graphql/daemon |
Graph Book operations use the public application endpoint unless an entry explicitly says otherwise. The daemon endpoint has a different schema and should not be substituted for application calls.
Examples will use placeholders such as YOUR_PLATFORM_TOKEN, COLLECTION_ID and ACCOUNT_ID. Never publish a live Platform token, wallet seed phrase or KEY_PASS.
How Entries Are Recorded
Each verified entry will include:
- the exact production query or mutation;
- required variables and authentication;
- a redacted example response;
- the practical use case;
- schema quirks, failure responses and retry behavior;
- whether it was verified on Canary or Enjin mainnet;
- the date and daemon or Platform version used for verification.
Restore a V2 Managed Wallet in V3
Verified 19 August 2026. A legacy managed wallet was successfully re-associated with V3 without moving its on-chain funds.
Managed-wallet records are Platform-side data and may not appear automatically in a new V3 account. The wallet itself is derived deterministically by the daemon from its master key and the original externalId. Recover that exact identifier before recreating the record.
1. Find the V2 External ID
Using the old V2 API token and schema, look up the funded account:
query FindLegacyManagedWallet($account: String!) {
GetWallet(account: $account) {
id
externalId
managed
network
account { address }
}
}
Do not confuse the V2 internal wallet id with externalId. Only the external ID is used for deterministic derivation.
2. Register the Same ID in V3
mutation RestoreManagedWallet($externalId: String!) {
CreateManagedWallet(externalId: $externalId)
}
The running V3 daemon polls the pending request, derives the wallet and returns its public key to Platform.
3. Verify Before Transacting
query ConfirmManagedWallet($externalId: String!) {
GetManagedWallet(externalId: $externalId) {
publicKey
externalId
}
}
Convert or compare the returned public key with the known funded SS58 address. They must identify the same account. A guessed external ID creates a different valid wallet; do not transact unless the key matches exactly.
Verified Queries
GetTransaction: Check Transaction State
Fully verified on Enjin mainnet on 19 August 2026. Used to follow both ENJ transfers and NFT creation from
PENDINGthrough toFINALIZED.
query GetTransactionStatus($uuid: String!) {
GetTransaction(
network: ENJIN
chain: MATRIX
uuid: $uuid
) {
uuid
idempotencyKey
state
error
createdAt
updatedAt
}
}
Variables
{
"uuid": "YOUR_TRANSACTION_UUID"
}
Verified Response Shape
{
"data": {
"GetTransaction": {
"uuid": "YOUR_TRANSACTION_UUID",
"idempotencyKey": "YOUR_IDEMPOTENCY_KEY",
"state": "FINALIZED",
"error": null,
"createdAt": "2026-08-19T13:06:04Z",
"updatedAt": "2026-08-19T13:06:21Z"
}
}
}
Store the UUID returned by CreateTransaction and poll this query. Do not treat the initial PENDING response as payment or mint completion. For the verified live operations in this guide, completion is recorded only after state is FINALIZED and error is null.
GetManagedWallet: Find a Daemon-Managed Wallet
Verified on Enjin mainnet on 19 August 2026. Used to confirm that a V2 managed wallet had been recreated from the correct original external ID.
query GetManagedWallet($externalId: String!) {
GetManagedWallet(externalId: $externalId) {
externalId
publicKey
}
}
Variables
{
"externalId": "YOUR_ORIGINAL_EXTERNAL_ID"
}
Response
{
"data": {
"GetManagedWallet": {
"externalId": "YOUR_ORIGINAL_EXTERNAL_ID",
"publicKey": "0xYOUR_64_CHARACTER_PUBLIC_KEY"
}
}
}
The returned public key must correspond to the expected funded SS58 address. If it does not, the external ID is wrong. A different external ID deterministically creates a different wallet even when the same daemon controls it.
GetAccount: Fetch Wallet Token Contents
Verified on Enjin mainnet on 19 August 2026. Used during player login to read NFTs from several collections and calculate the player's gameplay inventory.
Platform V3 replaces the V2 GetWallet.tokenAccounts.edges connection used for wallet contents with GetAccount.tokens. Query one collection per request and paginate it in blocks of 100.
query GetWalletCollectionTokens(
$address: String!
$collectionId: BigInt!
$after: Int!
) {
GetAccount(
network: ENJIN
chain: MATRIX
address: $address
) {
tokens(
collectionId: $collectionId
limit: 100
after: $after
) {
token {
tokenId
id
}
balance
}
}
}
Variables
{
"address": "PLAYER_MATRIXCHAIN_ADDRESS",
"collectionId": "3892",
"after": 0
}
Pagination
The maximum permitted limit is 100. Start with after: 0. If the response contains exactly 100 token accounts, request after: 100, then 200, and continue until a page contains fewer than 100 entries.
$after = 0;
$tokens = [];
do {
$variables = [
'address' => $wallet,
'collectionId' => (string) $collectionId,
'after' => $after,
];
$page = callEnjinGraphql($query, $variables);
$items = $page['data']['GetAccount']['tokens'] ?? [];
$tokens = array_merge($tokens, $items);
$after += 100;
} while (count($items) === 100);
Multiple Collections
Make a separate GraphQL request for each collection. During live testing, aliases for several tokens(collectionId: ...) fields inside one GetAccount selection returned the first collection's result under the other aliases. Separate requests for collections 2967, 3892 and 4110 returned the correct independent results.
Each entry contains a decimal tokenId, a canonical id in collectionId-tokenId form, and a string balance. Large legacy token IDs must remain strings; do not pass them through PHP integer or floating-point conversion.
Empty Wallet Results
{
"data": {
"GetAccount": {
"tokens": []
}
}
}
An empty tokens array is a successful read showing that the address owns no tokens from that collection. Treat GraphQL errors, transport failures, invalid JSON, or a missing GetAccount result as failures. In a login inventory synchronizer, preserve the player's saved inventory when a read fails; only replace it after every required collection and page has been fetched successfully.
Verified Mutations
RefreshMetadata: Re-fetch Token Metadata
Added for Enjin mainnet on 19 August 2026. Use after changing the JSON served by a token's metadata URI so Platform re-fetches the off-chain metadata.
Single Token
mutation RefreshMetadata {
RefreshMetadata(
collectionId: "4431"
tokenIds: "150"
)
}
GraphQL accepts the single tokenIds value above by coercing it to the mutation's list input. For reusable application code, pass an explicit list through variables:
mutation RefreshTokenMetadata(
$collectionId: BigInt!
$tokenIds: [BigInt!]!
) {
RefreshMetadata(
network: ENJIN
chain: MATRIX
collectionId: $collectionId
tokenIds: $tokenIds
)
}
Variables
{
"collectionId": "4431",
"tokenIds": ["150"]
}
Successful Response
{
"data": {
"RefreshMetadata": true
}
}
This refreshes Platform's cached off-chain metadata; it does not change the token's on-chain metadata URI. The mutation can also target canonical token IDs such as 4431-150 through ids, or target metadata URLs through uris.
CreateManagedWallet: Register a Managed Wallet
Verified on Enjin mainnet on 19 August 2026. Recreated a funded V2 managed wallet in Platform V3 by using its exact original external ID.
mutation CreateManagedWallet($externalId: String!) {
CreateManagedWallet(externalId: $externalId)
}
Variables
{
"externalId": "YOUR_ORIGINAL_EXTERNAL_ID"
}
The mutation registers the request with Platform. The running wallet daemon derives the managed wallet from its master seed and the external ID. Follow it with GetManagedWallet and verify the public key before sending any transaction.
CreateTransaction: Create a Single-Supply NFT
Fully verified on Enjin mainnet on 19 August 2026. Used in production to create Enjium champion NFTs and deliver the initial supply directly to the player's wallet.
mutation CreateNft(
$recipient: String!
$collectionId: BigInt!
$tokenId: BigInt!
$metadataUri: String!
$idempotencyKey: String!
) {
CreateTransaction(
network: ENJIN
chain: MATRIX
idempotencyKey: $idempotencyKey
transaction: {
createToken: {
recipient: $recipient
collectionId: $collectionId
tokenId: $tokenId
initialSupply: 1
listingForbidden: false
infusion: 0
anyoneCanInfuse: false
cap: {
type: COLLAPSING_SUPPLY
supply: 1
}
attributes: [
{ key: "uri", value: $metadataUri }
]
}
}
) {
uuid
idempotencyKey
state
}
}
Variables
{
"recipient": "PLAYER_MATRIXCHAIN_ADDRESS",
"collectionId": "YOUR_COLLECTION_ID",
"tokenId": "YOUR_UNIQUE_TOKEN_ID",
"metadataUri": "https://example.com/metadata/{id}",
"idempotencyKey": "your-nft-mint-record-123"
}
COLLAPSING_SUPPLY with a supply of 1 is the V3 replacement used for the old SINGLE_MINT behavior. Only one unit is created, and burning it permanently reduces the available cap so it cannot be re-minted.
Keep {id} literal when using Enjin's token-ID metadata substitution. Use the default daemon signer when the daemon wallet owns the collection; otherwise provide the appropriate managed-wallet signer. Store the returned UUID and confirm it with GetTransaction before marking the NFT as minted.
CreateTransaction: Send Native ENJ
Fully verified on Enjin mainnet on 19 August 2026. A funded V2 managed wallet was re-associated with the V3 daemon using its original external ID. An authenticated production
CreateTransactionrequest sent0.1ENJ using the decimalENJscalar, returned statePENDING, and reachedFINALIZEDwith no error 17 seconds after creation.
Platform V3 replaces the old top-level TransferKeepAlive mutation with CreateTransaction and a nested transferEnj transaction input.
| V2 | V3 |
|---|---|
TransferKeepAlive | CreateTransaction |
signingAccount | signerExternalId for a daemon-managed wallet |
| Amount supplied in 18-decimal base units | ENJ scalar supplied as a decimal ENJ string |
Returned id | Returns transaction uuid |
Mutation
mutation SendEnj(
$amount: ENJ!
$recipient: String!
$signerExternalId: String!
$idempotencyKey: String!
) {
CreateTransaction(
network: ENJIN
chain: MATRIX
signerExternalId: $signerExternalId
idempotencyKey: $idempotencyKey
transaction: {
transferEnj: {
amount: $amount
recipient: $recipient
}
}
) {
uuid
state
idempotencyKey
}
}
PHP Request
$amountToSend = trim((string) $amount_to_receive);
// ENJ accepts a decimal string with no more than 18 decimal places.
if (!preg_match('/^(?:0|[1-9][0-9]*)(?:\.[0-9]{1,18})?$/', $amountToSend)
|| preg_match('/^0(?:\.0{1,18})?$/', $amountToSend)) {
throw new InvalidArgumentException('Invalid ENJ withdrawal amount');
}
$query = <<<'GRAPHQL'
mutation SendEnj(
$amount: ENJ!
$recipient: String!
$signerExternalId: String!
$idempotencyKey: String!
) {
CreateTransaction(
network: ENJIN
chain: MATRIX
signerExternalId: $signerExternalId
idempotencyKey: $idempotencyKey
transaction: {
transferEnj: {
amount: $amount
recipient: $recipient
}
}
) {
uuid
state
idempotencyKey
}
}
GRAPHQL;
$data = [
'query' => $query,
'operationName' => 'SendEnj',
'variables' => [
'amount' => $amountToSend,
'recipient' => trim((string) $wallet),
'signerExternalId' => 'YOUR_MANAGED_WALLET_EXTERNAL_ID',
'idempotencyKey' => 'enj-withdrawal-' . $withdrawal_id,
],
];
$headers = [
'Authorization: Bearer ' . $matrix_token,
'Accept: application/json',
'Content-Type: application/json',
];
$jsonBody = json_encode($data, JSON_THROW_ON_ERROR);
Master and Managed Wallet Signers
For a funded managed wallet, use its restored externalId as signerExternalId; the withdrawal recipient remains the separate recipient value. V3 also accepts the managed wallet public key as signerAddress, but these two signer arguments are mutually exclusive. The external ID makes the daemon-managed relationship explicit.
Verified Final Response
{
"data": {
"GetTransaction": {
"uuid": "REDACTED_UUID",
"idempotencyKey": "YOUR_IDEMPOTENCY_KEY",
"state": "FINALIZED",
"error": null,
"createdAt": "2026-08-19T13:06:04Z",
"updatedAt": "2026-08-19T13:06:21Z"
}
}
}
For a normal single transfer, treat FINALIZED with error: null as success. Store and poll the transaction UUID rather than marking a withdrawal paid when its initial state is only PENDING.
Amount Units
Do not multiply the requested ENJ by 1000000000000000000. The V3 ENJ scalar accepts values such as "1", "1.5" or "0.000000000000000001" and converts them to base units internally. Keep monetary values as decimal strings in PHP; do not pass them through floating-point arithmetic.
Live-Operations Safety
- For this managed-wallet flow,
signerExternalIdidentifies the funded source account derived and controlled by the daemon. Verify its returned public key before enabling withdrawals. - Use a stable, unique withdrawal record identifier as the
idempotencyKey. If the same key is reported as already existing after a timeout, reconcile the original transaction rather than submitting again with a new key. - Store the returned
uuidbefore marking a withdrawal as submitted. - Do not mark the reward paid merely because
CreateTransactionreturned successfully; poll the transaction to its final on-chain state. - Test a minimal amount on Canary or with a tightly controlled production recipient before enabling the live queue.