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 Native ENJ Balance
Verified on Enjin mainnet on 24 August 2026. Used by a production withdrawal worker to refresh its managed hot wallet balance before checking and processing queued withdrawals.
query GetAccountEnjBalance($address: String!) {
GetAccount(address: $address) {
balance
}
}
Variables
{
"address": "ENJIN_MATRIXCHAIN_ADDRESS"
}
Verified Response Shape
{
"data": {
"GetAccount": {
"balance": "1002.089394527325066818"
}
}
}
The returned balance is native ENJ expressed as a decimal string with up to 18 decimal places. Preserve it as a string when validating it and writing it to a decimal database column; converting it to a PHP float first can lose precision.
For live operations, refresh the cached balance before checking whether the hot wallet can cover a withdrawal and again after the transfer reaches FINALIZED. If the API request fails, contains GraphQL errors or returns an invalid balance, log the failure and preserve the last known database value rather than overwriting it.
GetAccount: Fetch Wallet Token Contents
Verified in production on Enjin mainnet on 24 August 2026. Used during player login to fetch NFTs from several gameplay collections through one paginated query.
Platform V3 replaces the V2 GetWallet.tokenAccounts.edges connection with GetAccount.tokens. The production schema now accepts collectionIds: [BigInt!], allowing several collections to be filtered through one field without using GraphQL aliases or separate requests.
query GetGameplayWalletContents(
$address: String!
$collectionIds: [BigInt!]!
$after: Int!
) {
GetAccount(
network: ENJIN
chain: MATRIX
address: $address
) {
tokens(
collectionIds: $collectionIds
limit: 100
after: $after
) {
token {
tokenId
id
}
balance
}
}
}
Variables
{
"address": "PLAYER_MATRIXCHAIN_ADDRESS",
"collectionIds": ["2967", "3892", "4110"],
"after": 0
}
Although GraphQL may coerce a single value into this list input, pass an explicit array even when filtering only one collection. Keep BigInt identifiers as strings in JSON and PHP so large IDs are never passed through floating-point conversion.
Pagination
The maximum permitted limit is 100. Pagination applies to the combined results from every requested collection. Start with after: 0; when a page contains exactly 100 records, request after: 100, then 200, continuing until a page contains fewer than 100 records.
$after = 0;
$tokens = [];
do {
$variables = [
'address' => $wallet,
'collectionIds' => ['2967', '3892', '4110'],
'after' => $after,
];
$page = callEnjinGraphql($query, $variables);
$items = $page['data']['GetAccount']['tokens'] ?? [];
$tokens = array_merge($tokens, $items);
$after += 100;
} while (count($items) === 100);
Separate the Combined Results
Each token includes a canonical id in collectionId-tokenId form, such as 3892-1. Use the portion before the hyphen to route the record into the appropriate gameplay collection after every page has been fetched:
$collections = [
'2967' => [],
'3892' => [],
'4110' => [],
];
foreach ($tokens as $accountToken) {
$canonicalId = (string) ($accountToken['token']['id'] ?? '');
if (preg_match('/\A([0-9]+)-[0-9]+\z/', $canonicalId, $matches) !== 1) {
throw new UnexpectedValueException('Invalid canonical token ID');
}
if (isset($collections[$matches[1]])) {
$collections[$matches[1]][] = $accountToken;
}
}
This combined filter replaces the earlier workaround of requesting each collection separately. It normally reduces a wallet login from three Platform requests to one when the combined result contains fewer than 100 token accounts.
Empty and Failed Results
{
"data": {
"GetAccount": {
"tokens": []
}
}
}
An empty tokens array is a successful read showing that the address owns no tokens from the requested collections. Treat GraphQL errors, transport failures, invalid JSON, a missing GetAccount result or malformed canonical token IDs as failures. A login inventory synchronizer should preserve the player's saved inventory unless every required page has been fetched and classified 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.
Official References
Wallet Linking and User-Signed ENJ Payments
Production verified on Enjin Matrixchain on 3 September 2026. These operations linked Enjin Wallet accounts, requested native ENJ payments and supported completed Premium and NFT-backed Champion purchases.
The implementation and fulfilment model are documented in Enjin Platform V3: Wallet Linking and ENJ Payments.
CreateLinkingCode: Start Enjin Wallet Linking
mutation CreateWalletLink($idempotencyKey: String!) {
CreateLinkingCode(idempotencyKey: $idempotencyKey) {
idempotencyKey
qr
url
expires
}
}
Variables
{
"idempotencyKey": "wallet-link-user-123-attempt-1"
}
Store the key against the authenticated application account before submission. Display the returned QR code or open url in Enjin Wallet. Treat expires as the deadline for that linking attempt.
GetLinkedWallet: Reconcile a Linking Request
query GetLinkedWalletByKey($idempotencyKey: String!) {
GetLinkedWallet(idempotencyKey: $idempotencyKey) {
publicKey
idempotencyKey
}
}
Variables
{
"idempotencyKey": "wallet-link-user-123-attempt-1"
}
Poll by idempotency key until Platform returns a linked wallet. The returned publicKey is the authoritative account identity. Convert it to the Enjin Matrixchain SS58 representation, with prefix 1110, for display or storage where needed. Never replace this ownership proof with a wallet address typed into the application.
Lookup an Existing Link by Address
query GetLinkedWalletByAddress($address: String!) {
GetLinkedWallet(address: $address) {
publicKey
idempotencyKey
}
}
Supply exactly one selector: idempotencyKey while reconciling an account-specific link, or address when checking an already-known address. Compare complete public keys or decoded account IDs rather than shortened address text.
CreateTransaction: Request ENJ from a Linked Wallet
mutation RequestNativeEnjPayment(
$amount: ENJ!
$recipient: String!
$signerAddress: String!
$idempotencyKey: String!
) {
CreateTransaction(
network: ENJIN
chain: MATRIX
signerAddress: $signerAddress
idempotencyKey: $idempotencyKey
transaction: {
transferEnj: {
amount: $amount
recipient: $recipient
}
}
) {
uuid
state
idempotencyKey
}
}
Variables
{
"amount": "10.2500",
"recipient": "MERCHANT_MATRIXCHAIN_ADDRESS",
"signerAddress": "0xLINKED_WALLET_PUBLIC_KEY",
"idempotencyKey": "payment-order-456-attempt-1"
}
Unlike a daemon-signed withdrawal, this operation sets signerAddress to the public key returned by the wallet-linking flow. Platform sends the approval request to that linked Enjin Wallet. The merchant's receiving wallet is the separate recipient.
The amount is a decimal ENJ string. Do not multiply it by 10^18. Persist the intended idempotency key before calling Platform and reconcile that same key after an ambiguous timeout.
The initial state is not proof of payment. Store the returned UUID and poll the existing GetTransaction query. Fulfil only after FINALIZED with error: null.
CancelTransaction: Abandon an Unwanted Request
mutation CancelPaymentRequest($uuid: String!) {
CancelTransaction(uuid: $uuid) {
uuid
state
}
}
Variables
{
"uuid": "PAYMENT_TRANSACTION_UUID"
}
Use this when an uncompleted wallet request must be replaced. Do not submit its replacement until the old request reports ABANDONED. The replacement needs a new deterministic retry key; repeated submissions of that same retry attempt must reuse the new key.
There is no separate notification-resend operation in this verified flow. Safe resend means reconcile the existing transaction, cancel it when appropriate, confirm abandonment and create one replacement request.