# Chia Cloud Wallet API

## Getting started

The Cloud Wallet API is a single GraphQL endpoint for each network. Build and test
against testnet, then switch the host to mainnet. Both are production services, and
accounts, vaults, and API keys do not carry over between them.

| Network   | GraphQL over HTTP                        | Subscriptions (WebSocket)              |
| --------- | ---------------------------------------- | -------------------------------------- |
| Testnet11 | `https://api.vault.chiatest.net/graphql` | `wss://api.vault.chiatest.net/graphql` |
| Mainnet   | `https://api.vault.chia.net/graphql`     | `wss://api.vault.chia.net/graphql`     |

### Submit a request

Send a `POST` request with a JSON body containing `query` and, when the operation
declares them, `variables` and `operationName`:

```sh
curl https://api.vault.chiatest.net/graphql \
  -H 'Content-Type: application/json' \
  --data '{"query":"query Ping { ping }"}'
```

`ping` requires no authentication and returns `true` while the service is running:

```graphql
query Ping {
  ping
}
```

- Queries can also be sent with `GET` and URL parameters. Mutations must use `POST`.
- Send one operation per request. Batched request arrays are rejected with HTTP 400.
- Request bodies are limited to 3 MB. The limits on query size and shape are in the
  [conventions section](/docs#introduction-item-2).
- Subscriptions use the [`graphql-ws`](https://github.com/enisdenjo/graphql-ws) protocol
  on the same path. Only API-key authentication supports them; the
  [authentication section](/docs#introduction-item-1) shows how to sign the connection.
- Introspection is disabled in production. Download the
  [public schema](/docs/schema.graphql) for code generation and editor tooling
  instead.

### Make an authenticated request

`ping`, `viewer`, `oAuthScopes`, and `publicAnnouncements` work without
authentication. Treat every other operation as requiring an API key or an OAuth
access token. The [authentication section](/docs#introduction-item-1) explains how to sign requests with an
API key and how to obtain a token through OAuth. Once authenticated, confirm the
identity the API sees:

```graphql
query CurrentUser {
  viewer {
    id
    email
  }
}
```

A successful response has HTTP status 200 and a `data` object:

```json
{
  "data": {
    "viewer": {
      "id": "User_<cuid>",
      "email": "you@example.com"
    }
  }
}
```

If `viewer` is `null`, the API did not accept your credentials. `viewer` does not
return an error in that case, because it is public: an invalid OAuth token is
treated as no authentication. A bad API-key signature does return an
`UNAUTHENTICATED` error.

Errors are also returned with HTTP status 200, in an `errors` array next to `data`.
Check `errors` on every response, not only the status code. The
[conventions section](/docs#introduction-item-2) lists the error codes.

### Where to go next

- [Conventions](/docs#introduction-item-2) covers IDs, pagination, amounts, errors, and
  rate limits. They apply to every operation.
- [Vaults and transactions](/docs#introduction-item-3) lists vaults and sends XCH or a CAT.
- [Signature requests](/docs#introduction-item-4) explains how a transaction is
  signed and submitted.
- [Offers](/docs#introduction-item-5) creates, shares, takes, and cancels offers.
- [NFTs](/docs#introduction-item-6) mints and transfers NFTs.

The [API reference](/docs) documents each operation and type, grouped by topic, after
these guide sections. The [public schema](/docs/schema.graphql) contains the same
definitions in SDL.

## Authentication

Cloud Wallet supports API keys for direct user integrations and OAuth 2.0 for
applications that act with a user's consent. Neither method bypasses GraphQL
authorization, plan entitlements, or resource ownership.

| Capability            | API key                                        | OAuth                                                         |
| --------------------- | ---------------------------------------------- | ------------------------------------------------------------- |
| Intended client       | A user-controlled service or script            | A third-party web, desktop, or mobile application             |
| Identity              | The user who owns the key                      | The consenting user, narrowed to granted scopes and resources |
| Credential lifetime   | Until the selected expiration or deletion      | Short-lived access token; optional rotating refresh token     |
| GraphQL over HTTP     | Supported                                      | Supported                                                     |
| GraphQL subscriptions | Supported with signed WebSocket authentication | Not supported                                                 |

Do not send more than one authentication method on a request. A request that
contains a session cookie, bearer token, and signature header in any combination
is rejected.

### API keys

#### Create and manage a key

Sign in to Cloud Wallet and open **Settings > API Keys**.

1. Select **Create API Key**.
2. Enter a unique name.
3. Select an expiration of 30 days, 1 year, or never.
4. Create the key and save both the key ID and PEM private key.

Cloud Wallet stores the public key and displays the private key only once. Store
the private key in a secret manager. If you lose or expose it, delete the API key
and create another one. Deleting a key immediately prevents it from
authenticating new requests.

When the key is created, you can copy the PEM private key, copy its base64
encoding for storage in an environment variable, or download it as a `.pem`
file. The number of API keys you can create depends on your Cloud Wallet plan.

#### Security model

An API key is an asymmetric RSA key pair. Your client keeps the private key, and
Cloud Wallet stores only the public key used to verify signatures. The private
key is never sent with a request.

The signature covers the exact request body, nonce, and timestamp. Changing any
of those values invalidates the signature. The timestamp limits the replay
window, and each nonce can be used only once with a key.

#### Sign a request

API keys use RSA-SHA256 request signatures. For every request:

1. Serialize the GraphQL request body once.
2. Generate a unique nonce containing exactly 10 ASCII letters or digits.
3. Set the timestamp to the Unix epoch time in milliseconds.
4. Concatenate `requestBody + nonce + timestamp` without separators.
5. Sign that exact string with RSA-SHA256 and encode the signature as base64.
6. Send the exact serialized body that you signed.

| Header             | Value                                         |
| ------------------ | --------------------------------------------- |
| `content-type`     | `application/json; charset=utf-8`             |
| `chia-user-key-id` | The key ID shown when you created the API key |
| `chia-signature`   | Base64-encoded RSA-SHA256 signature           |
| `chia-nonce`       | Unique 10-character alphanumeric nonce        |
| `chia-timestamp`   | Unix epoch time in milliseconds               |

This Node.js example expects `CHIA_API_PRIVATE_KEY` to contain the raw PEM value
from **Copy** or **Download Key**. If your secret store contains the base64 copy,
decode it before passing it to `signer.sign`. The example always generates a
valid nonce and makes an authenticated GraphQL request to testnet:

```js
import { createSign, randomInt } from 'node:crypto';

const keyId = process.env.CHIA_API_KEY_ID;
const privateKey = process.env.CHIA_API_PRIVATE_KEY;
const nonceCharacters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';

function generateNonce() {
  let nonce = '';
  while (nonce.length < 10) {
    nonce += nonceCharacters.charAt(randomInt(nonceCharacters.length));
  }
  return nonce;
}

const body = JSON.stringify({
  query: 'query CurrentUser { viewer { id } }',
});
const nonce = generateNonce();
const timestamp = Date.now().toString();
const signer = createSign('RSA-SHA256');
signer.update(body + nonce + timestamp);
signer.end();

const response = await fetch('https://api.vault.chiatest.net/graphql', {
  method: 'POST',
  headers: {
    'content-type': 'application/json; charset=utf-8',
    'chia-user-key-id': keyId,
    'chia-signature': signer.sign(privateKey, 'base64'),
    'chia-nonce': nonce,
    'chia-timestamp': timestamp,
  },
  body,
});

console.log(await response.json());
```

This Python example uses the base64 copy of the private key:

```python
import base64
import json
import os
import secrets
import string
import time

import requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding

key_id = os.environ["CHIA_API_KEY_ID"]
private_key_pem = base64.b64decode(
    os.environ["CHIA_API_PRIVATE_KEY_BASE64"]
)
private_key = serialization.load_pem_private_key(
    private_key_pem,
    password=None,
)

body = json.dumps(
    {"query": "query CurrentUser { viewer { id } }"},
    separators=(",", ":"),
)
nonce_characters = string.ascii_letters + string.digits
nonce = "".join(secrets.choice(nonce_characters) for _ in range(10))
timestamp = str(int(time.time() * 1000))
canonical_string = body + nonce + timestamp
signature = private_key.sign(
    canonical_string.encode(),
    padding.PKCS1v15(),
    hashes.SHA256(),
)

response = requests.post(
    "https://api.vault.chiatest.net/graphql",
    headers={
        "content-type": "application/json; charset=utf-8",
        "chia-user-key-id": key_id,
        "chia-signature": base64.b64encode(signature).decode(),
        "chia-nonce": nonce,
        "chia-timestamp": timestamp,
    },
    data=body,
    timeout=30,
)

print(response.json())
```

The timestamp must be within five minutes of the server time. Cloud Wallet
rejects a nonce that has already been used with the same key.

#### Authenticate a subscription

A WebSocket connection has no request body, so sign the empty string followed by the
nonce and timestamp, and send the four `chia-` headers as `connectionParams`. Sign
each connection attempt afresh, because a nonce cannot be reused. This example uses
the [`graphql-ws`](https://github.com/enisdenjo/graphql-ws) client and the
`generateNonce` function from the previous example:

```js
import { createSign } from 'node:crypto';
import { createClient } from 'graphql-ws';

const client = createClient({
  url: 'wss://api.vault.chiatest.net/graphql',
  connectionParams: () => {
    const nonce = generateNonce();
    const timestamp = Date.now().toString();
    const signer = createSign('RSA-SHA256');
    signer.update(nonce + timestamp);
    signer.end();
    return {
      'chia-user-key-id': keyId,
      'chia-signature': signer.sign(privateKey, 'base64'),
      'chia-nonce': nonce,
      'chia-timestamp': timestamp,
    };
  },
});

client.subscribe(
  { query: 'subscription WalletUpdated { walletUpdated { id name } }' },
  {
    next: (result) => {
      if (result.errors) console.error(result.errors);
      else console.log(result.data);
    },
    error: console.error,
    complete: () => {},
  },
);
```

A failed authentication appears in one of two ways, so handle both:

- An invalid signature, nonce, or timestamp closes the connection with close code
  `4500` and no GraphQL error. The server acknowledges the connection first, so a
  `connected` event does not mean authentication succeeded. The client reports the close through `error` and does
  not reconnect, so fix the signing code instead of retrying.
- A connection without credentials is accepted. Each subscription then receives a
  result with an `errors` array and an `UNAUTHENTICATED` code through `next`,
  followed by `complete`, so check `errors` on every result.

`walletUpdated` reports changes to every vault the key's user owns. Filter by `id`
in your handler. `graphql-ws` parses messages with `JSON.parse`, which rounds
`BigInt` values, so do not select amounts in a subscription: read them again over
HTTP when an event arrives.

#### Troubleshoot API-key authentication

Authentication failures use the GraphQL `UNAUTHENTICATED` error code.

| Error                                                   | Cause                                                            | Resolution                                                                                                                                                   |
| ------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Begins `Invalid signature`                              | The signature, key ID, or exact body does not match              | Build the signature from the exact body string sent, followed by the nonce and timestamp. Sign the body after serializing it, and send those bytes unchanged |
| `Nonce must be exactly 10 alphanumeric characters long` | The nonce has the wrong length or contains punctuation           | Generate exactly 10 characters from `A-Z`, `a-z`, and `0-9`                                                                                                  |
| `Nonce has already been used.`                          | The key already authenticated a request with this nonce          | Generate a new nonce for every request                                                                                                                       |
| `Timestamp is too far away from the current time`       | The timestamp differs from server time by more than five minutes | Synchronize the client clock and send milliseconds since the Unix epoch                                                                                      |
| `Auth Key has expired`                                  | The selected key expiration has passed                           | Delete the expired key and create another one                                                                                                                |
| `Multiple tokens provided`                              | The request contains more than one authentication method         | Send only the API-key headers                                                                                                                                |

An unknown key ID returns the same response as an invalid signature. This
prevents key enumeration.

### OAuth authorization code with PKCE

OAuth uses the authorization-code flow with PKCE (`S256`). It is appropriate
when your application needs access to another user's Cloud Wallet resources.

#### Register a client

When the developer-registration rollout is enabled, organization administrators
can submit an application from **Settings > OAuth Apps**. New submissions
remain pending and cannot authorize users until Cloud Wallet approves them.
Environments without the rollout continue to use manual registration.

These instructions assume you received a client ID and registered one or more
redirect URIs. One registered URI must exactly match the `redirect_uri`
supplied during authorization and token exchange.

Public clients use PKCE and do not use a client secret. Never embed a
confidential client secret in browser, desktop, or mobile code. The
registration portal displays a generated secret once for future confidential
client support, but the OAuth server does not accept it for authentication.

The endpoints are on the same host as GraphQL. The examples below use testnet;
replace `api.vault.chiatest.net` with `api.vault.chia.net` for mainnet.

| Purpose                    | Method and path  |
| -------------------------- | ---------------- |
| Authorization              | `GET /authorize` |
| Token exchange and refresh | `POST /token`    |
| Token revocation           | `POST /revoke`   |
| GraphQL                    | `POST /graphql`  |

OAuth libraries that read server metadata can use
`/api/auth/.well-known/oauth-authorization-server` on the same host. It advertises
equivalent endpoints under `/api/auth/oauth2/`, and the issuer
`https://<host>/api/auth`, which the callback's `iss` parameter carries.

#### Choose scopes and resources

Request only the permission scopes your application needs, separated by spaces.
Permission scopes use names such as `wallet.read`, `wallet.write`, and
`transfer.create`. Query the public scope catalog for the available scopes,
descriptions, and resource types:

```graphql
query AvailableOAuthScopes {
  oAuthScopes(first: 100) {
    edges {
      node {
        scope
        description
        resourceType
        resourceGrantNotRequired
      }
    }
  }
}
```

The workflows in this guide need these scopes:

| Operation                                                                         | Scope                     |
| --------------------------------------------------------------------------------- | ------------------------- |
| Read vaults, balances, transactions, and signature requests                       | `wallet.read`             |
| Read a vault's NFTs                                                               | `nft.read`                |
| Read a vault's offers, including `walletOffer`                                    | `offer.read`              |
| `deriveVaultAddress`, `resendSignatureRequest`                                    | `wallet.write`            |
| `createTransaction`, `clawBackTransaction`, `finalizeTransaction`, `transferNfts` | `transfer.create`         |
| `deleteTransaction`                                                               | `transfer.delete`         |
| `signSignatureRequest`                                                            | `wallet.sign`             |
| `submitSignatureRequest`                                                          | `signatureRequest.submit` |
| `createOffer`                                                                     | `offer.create`            |
| `addOffer`, `takeOffer`                                                           | `offer.accept`            |
| `cancelOffer`                                                                     | `offer.cancel`            |
| `bulkMintNfts`                                                                    | `nft.create`              |

When a scope applies to wallets or another resource type, the consent screen
asks the user which resources to grant. A token can use a permission only when
the user has that permission and consented to the required resource. Consent
narrows the user's existing access; it does not create new access.

Include `offline_access` when your application needs a refresh token. The legacy
`read_vault` and `write_vault` scopes are deprecated; use granular permission
scopes for new integrations.

#### Authorize the user

Generate a cryptographically random `state` value and PKCE verifier. Store both
until the callback. Derive the `S256` challenge as
`base64url(sha256(code_verifier))`, without base64 padding.

An RFC 7636 verifier is 43–128 characters from the unreserved URI character
set. This Node.js example generates a 64-character verifier with 384 bits of
randomness, derives its challenge, and constructs the authorization URL:

```js
import { createHash, randomBytes, randomUUID } from 'node:crypto';

const clientId = process.env.CHIA_OAUTH_CLIENT_ID;
const redirectUri = process.env.CHIA_OAUTH_REDIRECT_URI;
const state = randomUUID();
const codeVerifier = randomBytes(48).toString('base64url');
const codeChallenge = createHash('sha256').update(codeVerifier).digest('base64url');

const authorizeUrl = new URL('https://api.vault.chiatest.net/authorize');
authorizeUrl.searchParams.set('client_id', clientId);
authorizeUrl.searchParams.set('redirect_uri', redirectUri);
authorizeUrl.searchParams.set('response_type', 'code');
authorizeUrl.searchParams.set('scope', 'wallet.read offline_access');
authorizeUrl.searchParams.set('state', state);
authorizeUrl.searchParams.set('code_challenge', codeChallenge);
authorizeUrl.searchParams.set('code_challenge_method', 'S256');

// Store state and codeVerifier in the user's server-side session before redirecting.
console.log(authorizeUrl.toString());
```

Redirect the user's browser to `/authorize` with these query parameters:

```text
https://api.vault.chiatest.net/authorize
  ?client_id=<client_id>
  &redirect_uri=<registered_redirect_uri>
  &response_type=code
  &scope=wallet.read%20offline_access
  &state=<random_state>
  &code_challenge=<pkce_s256_challenge>
  &code_challenge_method=S256
```

After Cloud Wallet redirects to your callback:

1. Reject the response if its `state` does not exactly match the stored value.
2. Handle an OAuth `error` response instead of assuming a code is present.
3. Exchange the authorization `code` immediately. A code is single-use.

```sh
curl https://api.vault.chiatest.net/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=authorization_code' \
  --data-urlencode 'client_id=<client_id>' \
  --data-urlencode 'code=<authorization_code>' \
  --data-urlencode 'redirect_uri=<registered_redirect_uri>' \
  --data-urlencode 'code_verifier=<pkce_verifier>'
```

The JSON response contains `access_token` and `token_type`. Use `expires_in` to
schedule renewal when it is present. A grant that includes `offline_access`
also returns `refresh_token`; replace it after every successful refresh. The
`scope` value, when present, records the scopes granted to the token.

Send the resulting access token as a bearer token to make the first
authenticated GraphQL request:

```sh
curl https://api.vault.chiatest.net/graphql \
  -H 'Authorization: Bearer <access_token>' \
  -H 'Content-Type: application/json' \
  --data '{"query":"query CurrentUser { viewer { id } }"}'
```

Authorization failures redirect to the registered callback with `error` and
may include `error_description`. Token endpoint failures return a non-2xx JSON
response with the same fields. Handle errors such as `invalid_request`,
`invalid_client`, `invalid_grant`, `invalid_scope`, and `invalid_target`
without exposing their descriptions to untrusted users. An `invalid_grant`
during code exchange or refresh requires a new authorization flow.

#### Refresh and revoke tokens

If authorization included `offline_access`, exchange the refresh token at the
token endpoint:

```sh
curl https://api.vault.chiatest.net/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=refresh_token' \
  --data-urlencode 'client_id=<client_id>' \
  --data-urlencode 'refresh_token=<refresh_token>'
```

A successful refresh rotates the refresh token. Store the new access token and
refresh token atomically, and discard the previous refresh token.

Revoke an access token or refresh token when the user disconnects the
application:

```sh
curl https://api.vault.chiatest.net/revoke \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'client_id=<client_id>' \
  --data-urlencode 'token=<access_or_refresh_token>'
```

Revoking a refresh token also prevents future access-token refreshes. Your
application must treat a rejected refresh as a disconnected authorization and
send the user through authorization again.

#### OAuth restrictions

- OAuth access tokens authenticate GraphQL requests over HTTP.
- OAuth sessions are rejected during the GraphQL WebSocket handshake. OAuth
  applications cannot use GraphQL subscriptions.
- The token can access only operations supported for OAuth and only the scopes
  and resources recorded during consent.
- Account-security and connected-application management operations can require
  a direct user session and reject delegated OAuth access.

## Conventions

These conventions apply to every operation. The reference documents individual
fields; this section documents the rules they share.

### Global IDs

Every object ID is a global ID in `TypeName_cuid` format, such as `Wallet_<cuid>`,
`SignatureRequest_<cuid>`, or `WalletTransaction_<cuid>`. The part before the first
underscore names the type. Global IDs are plain strings, not base64-encoded Relay IDs.

Treat IDs as opaque. Pass back the exact value the API returned, and do not construct
an ID from its parts. An ID argument accepts only the type it documents:

| Input                                | Error code       | Message                                  |
| ------------------------------------ | ---------------- | ---------------------------------------- |
| No underscore                        | `BAD_USER_INPUT` | `Invalid globalId format`                |
| Another type, such as a `Signer_` ID | `BAD_USER_INPUT` | `ID: <value> is not of type: <Expected>` |

A few identifiers are not CUIDs. For example, an offer has two IDs: `WalletOffer.id`
is a `WalletOffer_<cuid>` ID for your vault's copy of the offer, which
[`walletOffer`](/docs#query-walletOffer) accepts, and `WalletOffer.offerId` is an
`Offer_<hex>` ID containing the offer's on-chain hash, which `takeOffer` and
`cancelOffer` accept. The reference states the format wherever an argument expects
one.

### Cursor pagination

List fields are Relay-style connections. Page forward with `first` and `after`, or
backward with `last` and `before`:

```graphql
query ListVaultsPage($after: String) {
  viewer {
    wallets(first: 50, after: $after) {
      totalCount
      pageInfo {
        hasNextPage
        endCursor
      }
      edges {
        cursor
        node {
          id
          name
        }
      }
    }
  }
}
```

- A page holds at most 100 items. Most connections return 10 items when you omit
  `first` and `last`, and reject a larger value with `BAD_USER_INPUT` and the message
  `Limit 101 is greater than max 100`. A few connections over small, fixed lists, such
  as [`oAuthScopes`](/docs#query-oAuthScopes), return 20 items by default and return
  100 items when you ask for more. Always pass `first` and follow `hasNextPage`
  rather than relying on either behavior.
- Pass `pageInfo.endCursor` as `after` to fetch the next page. Stop when
  `hasNextPage` is `false`.
- Cursors are opaque and are valid only for the connection and sort order that
  produced them. A cursor you did not receive from the API fails with
  `BAD_USER_INPUT`; the message depends on the connection.
- Every connection exposes `totalCount`. Counting has a cost, so request it only when
  you need it.

### Amounts and units

Amounts are integers in mojos, the smallest unit of each asset. Never send decimal
XCH or CAT amounts.

| Asset | Mojos per unit    | Example: 1.5 units |
| ----- | ----------------- | ------------------ |
| XCH   | 1,000,000,000,000 | `1500000000000`    |
| CAT   | 1,000             | `1500`             |

Fees are always XCH mojos, including on CAT transfers and offers. A CAT is identified
by its `assetHash`, the 64-character hexadecimal tail hash; omit `assetHash` for XCH.
Convert with the fixed ratios in this table rather than with an asset's `decimals`
field.

### Scalars

| Scalar     | Input                                                     | Output                               |
| ---------- | --------------------------------------------------------- | ------------------------------------ |
| `BigInt`   | An integer, or a decimal string such as `"1000000000000"` | A JSON number, which can exceed 2^53 |
| `DateTime` | An ISO 8601 date and time, such as `2026-01-31T12:00:00Z` | An ISO 8601 string in UTC            |
| `Bytea`    | Hexadecimal, with or without a `0x` prefix                | Hexadecimal without a prefix         |
| `JSON`     | Any JSON value                                            | Any JSON value                       |

**`BigInt` output needs a precision-safe JSON parser.** Mojo amounts routinely exceed
`Number.MAX_SAFE_INTEGER`, and the API writes them as bare JSON numbers. A standard
`JSON.parse` in JavaScript silently rounds them, producing a wrong balance rather
than an error. In JavaScript, parse responses with a library such as
[`json-bigint`](https://www.npmjs.com/package/json-bigint), or call `JSON.parse`
with a reviver that reads the source text, where the runtime supports it. Python's
`json` module keeps integers exact. For input, a value above 2^53 − 1 must be a
decimal string, because a JSON number that large cannot be represented exactly.
Sending every amount as a decimal string is the simplest way to satisfy this.

Many timestamp fields, such as `createdAt` and `updatedAt` on `Wallet`, are typed
`String` rather than `DateTime`. They use the format `2026-01-31 12:00:00.123456`,
with a space instead of `T` and no time zone offset, and the values are UTC. Append
`Z` after replacing the space if your parser requires ISO 8601.

### Defaults

An optional argument you omit takes the default shown in the reference, such as
`autoSubmit: Boolean = true`. When an input field has no default:

- An omitted `fee` is zero. A transaction with a zero fee can take longer to confirm
  when the network is busy. The API does not estimate fees, so choose the fee in your
  application.
- An omitted `first` and `last` returns a page of 10 items on most connections.

`autoSubmit` defaults differ between mutations, because some flows expect a review
step before anything reaches the network:

- **Default `true`:** [`createTransaction`](/docs#mutation-createTransaction) and
  [`createOffer`](/docs#mutation-createOffer). For `createOffer`, `autoSubmit` applies
  only to the coin split made with `splitInputCoins`; see the [offers section](/docs#introduction-item-5).
- **Default `false`:** [`takeOffer`](/docs#mutation-takeOffer),
  [`cancelOffer`](/docs#mutation-cancelOffer),
  [`bulkMintNfts`](/docs#mutation-bulkMintNfts),
  [`transferNfts`](/docs#mutation-transferNfts),
  [`finalizeTransaction`](/docs#mutation-finalizeTransaction), and
  [`clawBackTransaction`](/docs#mutation-clawBackTransaction).

Pass `autoSubmit` explicitly whenever the difference matters to your integration.

### Errors

The API follows the GraphQL response format. An operation that fails returns HTTP
status 200 with an `errors` array. Each error has a `message` and, almost always, an
`extensions.code`. Fields that resolved successfully still appear in `data`, so a
response can contain both.

```json
{
  "data": { "wallet": null },
  "errors": [
    {
      "message": "ID: Signer_<cuid> is not of type: Wallet",
      "extensions": { "code": "BAD_USER_INPUT" }
    }
  ]
}
```

Branch on `extensions.code`, not on `message`. Messages are for people and can change.

- Do not rely on `path`. Most errors omit it, so send one root field per request when
  you need to know which field failed.
- Some errors have no `extensions` at all: the alias and directive limits described
  under query limits, and a variable that is missing or has the wrong type. Treat an
  error without a code as a problem with the request document or its variables.

| Code                           | Meaning                                                                                                                            |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| `UNAUTHENTICATED`              | The operation requires authentication and the request has none, or its API-key signature is invalid                                |
| `UNAUTHORIZED`                 | The caller lacks the permission, or the resource does not exist or is not visible to the caller                                    |
| `BAD_USER_INPUT`               | An argument is invalid; the message explains which                                                                                 |
| `BAD_REQUEST`                  | The HTTP request is malformed, such as a batched array or a mutation sent with `GET`                                               |
| `OPERATION_RESOLUTION_FAILURE` | `operationName` does not name an operation in the document                                                                         |
| `GRAPHQL_PARSE_FAILED`         | The document is not valid GraphQL, or exceeds the token limit                                                                      |
| `GRAPHQL_VALIDATION_FAILED`    | The document does not match the schema                                                                                             |
| `RATE_LIMIT_EXCEEDED`          | The operation's rate limit is exhausted                                                                                            |
| `COMPLEXITY_EXCEEDED`          | The query is too deep, too broad, or too expensive                                                                                 |
| `PLAN_LIMIT_MET`               | The account's plan does not allow more of this resource                                                                            |
| `DUPLICATE_KEY`                | A resource with the same unique value already exists                                                                               |
| `AUTO_SUBMIT_FAILED`           | The request was fully signed, but automatic submission failed; see the [signature requests section](/docs#introduction-item-4) |
| `CIRCUIT_BREAKER_ACTIVE`       | Cloud Wallet temporarily paused this operation                                                                                     |
| `UNSUPPORTED_OFFER`            | The offer uses an outdated format that can no longer be accepted                                                                   |
| `RPC_ERROR`                    | The Chia network rejected the request                                                                                              |
| `INTERNAL_SERVER_ERROR`        | An unexpected failure. The message is always `Unexpected error.`                                                                   |

There is no `NOT_FOUND` code. An ID for a resource that does not exist, or that you
cannot access, returns `null` with `UNAUTHORIZED`; the API does not reveal which.

An invalid, expired, or revoked OAuth access token does not produce an error. The
request runs unauthenticated: public fields such as `viewer` return `null`, and
other fields fail with `UNAUTHENTICATED`. A `null` `viewer` therefore means the API
did not accept your credentials.

An `INTERNAL_SERVER_ERROR` hides its cause deliberately. Retry a query once. Do not
retry a mutation blindly, because the error does not say whether the change took
effect: read the affected resource first, for example the vault's `transactions`,
and retry only if the change is absent. If the error persists, contact support with
the time of the request.

Requests rejected before GraphQL runs use HTTP status codes instead:

| HTTP status | Cause                                       | Message                                                      |
| ----------- | ------------------------------------------- | ------------------------------------------------------------ |
| 400         | A batched request array                     | `Batching is not supported.`                                 |
| 400         | Variables exceed the size limits below      | `Input validation failed`                                    |
| 400         | A variable is missing or has the wrong type | Names the variable, as in `Variable "$input" …`              |
| 400         | `operationName` matches no operation        | `Could not determine what operation to execute.`             |
| 405         | A mutation sent with `GET`                  | `Can only perform a mutation operation from a POST request.` |
| 413         | A request body over 3 MB                    | None                                                         |

### Rate limits

Each root field has its own budget per user, or per IP address for unauthenticated
requests. Every call costs 10 points. A budget's 60-second window starts with the
first call and resets when it ends.

| Operation type | Points per 60 seconds | Calls per 60 seconds |
| -------------- | --------------------- | -------------------- |
| Queries        | 1,000                 | 100                  |
| Mutations      | 500                   | 50                   |

Because the budget is per root field, calling `wallet` 100 times does not reduce what
remains for `viewer`. Authentication and verification operations have stricter
limits. Some accounts have a higher allowance by agreement.

An exhausted budget returns `RATE_LIMIT_EXCEEDED` with HTTP status 200. No
`Retry-After` header is sent; the message states the time until the window resets, as
in `Rate limit exceeded, please try again in 42 seconds`. Back off until then instead
of retrying immediately. The limit is checked before the field runs, so a
rate-limited mutation made no change and is safe to retry after the wait.

### Query limits

These limits protect the service and apply to every request:

| Limit                        | Value                                      | Error                                       |
| ---------------------------- | ------------------------------------------ | ------------------------------------------- |
| Query document length        | 100,000 characters                         | HTTP 400, `Input validation failed`         |
| Keys in one variables object | 100                                        | HTTP 400, `Input validation failed`         |
| Nesting depth of variables   | 15                                         | HTTP 400, `Input validation failed`         |
| Items in one variables array | 10,000                                     | HTTP 400, `Input validation failed`         |
| Items in one list argument   | 250, unless the reference states otherwise | `BAD_USER_INPUT`                            |
| Aliases                      | 15                                         | No code; the message begins `Syntax Error:` |
| Directives                   | 50                                         | No code; the message begins `Syntax Error:` |
| Tokens                       | 1,000                                      | `GRAPHQL_PARSE_FAILED`                      |
| Selection depth              | 25                                         | `COMPLEXITY_EXCEEDED`                       |
| Selection breadth            | 400                                        | `COMPLEXITY_EXCEEDED`                       |
| Complexity                   | 5,000                                      | `COMPLEXITY_EXCEEDED`                       |

Each field costs 1 complexity point, and a list or connection multiplies the cost of
its selection by 10. The edges of `WalletTransaction.assetChanges` multiply by 3, because
most transactions change few assets. Request only the fields you use, and paginate instead of nesting
several connections.

## Vaults and transactions

Cloud Wallet calls wallets "vaults" in the user interface. The GraphQL schema uses
`Wallet` and `wallets`.

### List vaults

```graphql
query ListVaults {
  viewer {
    wallets(first: 20) {
      edges {
        node {
          id
          name
          state
          xch: asset(assetHash: null) {
            totalAmount
            spendableAmount
          }
        }
      }
    }
  }
}
```

`asset(assetHash: null)` selects the vault's XCH balance. Pass `assetHash: null`
explicitly: an `asset` field with no `assetHash` argument returns `null`. Pass a
CAT's `assetHash` to read that CAT instead. A vault in the `READY` state can send
and receive. `VAULT_PENDING_CREATE` means the vault's creation has not confirmed on
chain yet, and the two `VAULT_RECOVERY_` states mean a recovery is in progress.

To read one vault, pass its ID to [`wallet`](/docs#query-wallet). Its `assets` connection lists
each XCH and CAT balance in mojos, and its `transactions` connection lists its
history. `totalAmount` is the full balance. `spendableAmount` excludes
`lockedAmount`, the coins reserved by transactions and offers that have not settled.

```graphql
query VaultActivity($walletId: ID!) {
  wallet(id: $walletId) {
    assets(first: 20) {
      edges {
        node {
          assetHash
          symbol
          totalAmount
          spendableAmount
        }
      }
    }
    transactions(first: 20) {
      edges {
        node {
          id
          type
          state
          walletFee
          assetChanges(first: 10) {
            edges {
              node {
                assetHash
                direction
                amount
              }
            }
          }
          signatureRequest {
            id
            status
          }
        }
      }
    }
  }
}
```

`assetChanges` lists each asset a transaction changed for the vault, one node per
asset, so an offer shows both what the vault gave and what it received. `amount` is
in mojos and never negative, and `direction` says whether the vault gained or lost the
asset. `assetHash` is null for XCH. NFT changes, and an NFT launcher id passed to
`changedAssetHash`, also need `nft.read` or `wallet.sign` on the vault; without either,
the change is left out and the filter matches nothing. `walletFee` is the network fee the vault paid, in
XCH mojos.

### Receive funds

A vault receives XCH, CATs, and NFTs at its vault addresses. `vaultAddresses` lists
them, and index 0 is the primary receive address:

```graphql
query VaultAddresses($walletId: ID!) {
  wallet(id: $walletId) {
    vaultAddresses {
      index
      address
      label
    }
  }
}
```

[`deriveVaultAddress`](/docs#mutation-deriveVaultAddress) adds a new address at the
next index, for example to give each payer a separate address. Do not use
`Wallet.address` as a receive address: it returns one of the wallet's puzzle hashes,
which is not necessarily the primary receive address.

Addresses begin with `txch1` on testnet and `xch1` on mainnet. The examples in this
guide use `xch1...`; substitute a testnet address when you test.

### Create a transaction

[`createTransaction`](/docs#mutation-createTransaction) sends XCH or a CAT from a vault to an address:

```graphql
mutation CreateTransaction($input: CreateTransactionInput!) {
  createTransaction(input: $input) {
    signatureRequest {
      id
      status
      transaction {
        id
        state
      }
    }
  }
}
```

This input sends 1 XCH. Omit `assetHash` for XCH:

```json
{
  "input": {
    "walletId": "Wallet_<cuid>",
    "address": "xch1...",
    "amount": "1000000000000",
    "fee": "0",
    "autoSubmit": true
  }
}
```

This input sends 2.5 units of a CAT. The amount uses CAT mojos, and the fee is still
XCH mojos:

```json
{
  "input": {
    "walletId": "Wallet_<cuid>",
    "address": "xch1...",
    "assetHash": "<64-character-cat-tail-hash>",
    "amount": "2500",
    "fee": "1000000"
  }
}
```

Omit `type`: only `TRANSFER` is supported, and it is the default. `xchHandle` is
optional; if you resolved the address from an XCHandle, pass the handle and the
mutation fails if it no longer resolves to `address`.

`createTransaction` does not sign or send anything. It creates two linked objects
immediately:

- A `WalletTransaction` in the `UNSIGNED` state, which appears in the vault's
  `transactions` connection.
- A `SignatureRequest` in the `UNSIGNED` status, which collects the signatures the
  vault's signers must supply.

The [signature requests section](/docs#introduction-item-4) explains how signing and submission work, and how
`autoSubmit` controls the last step.

### Transaction states

`WalletTransaction.state` moves forward as the transaction is signed, submitted, and
confirmed:

```text
UNSIGNED → SIGNED → MEMPOOL → SETTLED
                         ↘ INVALID
```

| State         | Meaning                                                                                            |
| ------------- | -------------------------------------------------------------------------------------------------- |
| `UNSIGNED`    | Waiting for signatures                                                                             |
| `SIGNED`      | Every required signature is collected, and the transaction has not been submitted                  |
| `MEMPOOL`     | Submitted to the network and waiting to be included in a block                                     |
| `PENDING`     | In progress and not yet confirmed; treat it like `MEMPOOL`                                         |
| `SETTLED`     | Confirmed on chain. This is final for most transactions                                            |
| `RE_ORG`      | A chain reorganization removed the confirming block; the transaction is waiting to confirm again   |
| `INVALID`     | The network rejected the transaction, or one of its coins was spent elsewhere. It will not confirm |
| `FINALIZED`   | A clawback transaction was released to the recipient                                               |
| `CLAWED_BACK` | A clawback transaction was returned to the sender                                                  |

Poll [`walletTransaction`](/docs#query-walletTransaction) to follow a transaction.
Chia adds a transaction block about every 52 seconds on average, so polling every 10
to 30 seconds is enough and stays well within the rate limits. API-key clients can
also subscribe to [`walletUpdated`](/docs#subscription-walletUpdated) and read the transaction again when the vault
changes; the [authentication section](/docs#introduction-item-1) shows how to connect.

### Remove an unsent transaction

[`deleteTransaction`](/docs#mutation-deleteTransaction) removes a transaction in the `UNSIGNED`, `SIGNED`, or
`INVALID` state from the vault's history and cancels its signature request. A
transaction that has been submitted cannot be deleted, and the mutation fails with
`Transaction has been submitted, it can not be deleted`. A deleted transaction
reappears if it later confirms on chain.

### Clawback transactions

Set `clawback.timestamp` on `CreateTransactionInput` to send a clawback transaction.
Until that time, the sender can reclaim the funds. Read
`WalletTransaction.clawbackState` to see which action the sender can take:

- [`clawBackTransaction`](/docs#mutation-clawBackTransaction) returns the funds to the sender while
  `canClawback` is `true`, which ends when the timer expires.
- [`finalizeTransaction`](/docs#mutation-finalizeTransaction) pushes the funds to the recipient. Before the
  timer expires this requires `canFinalize`; afterward it requires `canPushThrough`.

Both create a new signature request and default to `autoSubmit: false`.

## Signature requests

Every operation that spends from a vault, including transfers, offers, NFT mints, and
clawback actions, returns a `SignatureRequest` instead of sending anything. Cloud
Wallet never signs on a vault's behalf. The vault's signers produce the signatures,
and Cloud Wallet verifies them, aggregates them, and submits the spend.

### Signature request statuses

```text
UNSIGNED → SIGNED → SUBMITTED
    ↘          ↘
     CANCELLED ← (transaction deleted or invalidated)
```

`SignatureRequest.status` is a string with one of these values:

| Status            | Meaning                                                                                         |
| ----------------- | ----------------------------------------------------------------------------------------------- |
| `AWAITING_REVIEW` | Waiting for a recovery provider to review it. It cannot be signed yet                           |
| `UNSIGNED`        | Collecting signatures. Partial signatures accumulate while the request stays in this status     |
| `SIGNED`          | Every required signature is collected, and the spend has not been submitted                     |
| `SUBMITTED`       | Submitted to the network. Follow the linked `transaction` for confirmation                      |
| `CANCELLED`       | The transaction was deleted or became invalid. The request can no longer be signed or submitted |
| `PROCESSING`      | Reserved; no current flow sets it. Do not submit a request in this status; read it again later  |

`SUBMITTED` means the network accepted the spend into its mempool, not that it
confirmed. Confirmation is reported on `SignatureRequest.transaction.state`, as
described in the [vaults and transactions section](/docs#introduction-item-3).

A vault spend needs signatures from as many signers as the vault's custody policy
requires, which can be fewer than all of them. Cloud Wallet moves the request to
`SIGNED` as soon as the policy is satisfied.

### Find requests to sign

Every mutation that creates a request returns its ID. To find requests created
elsewhere, list the vault's `pinnedTransactions`, which holds unsigned transactions
and rejected ones you can retry, and read each transaction's `signatureRequest`:

```graphql
query PendingSignatureRequests($walletId: ID!) {
  wallet(id: $walletId) {
    pinnedTransactions(first: 20) {
      edges {
        node {
          id
          type
          signatureRequest {
            id
            status
          }
        }
      }
    }
  }
}
```

API-key clients can also subscribe to
[`signatureRequestCreated`](/docs#subscription-signatureRequestCreated), as described
under signing.

### Sign a request

Read the messages the request needs signed:

```graphql
query SignatureRequestMessages($id: ID!) {
  signatureRequest(id: $id) {
    id
    status
    autoSubmit
    messages {
      curve
      publicKey
      message
    }
  }
}
```

Each entry names a public key and the message that key must sign. Sign each message
with the matching private key outside Cloud Wallet, for example in the Chia Signer
app or your own key management system. Then send each signature with
[`signSignatureRequest`](/docs#mutation-signSignatureRequest):

```graphql
mutation SignSignatureRequest($input: SignSignatureRequestInput!) {
  signSignatureRequest(input: $input) {
    signatureRequest {
      id
      status
    }
  }
}
```

```json
{
  "input": {
    "signatureRequestId": "SignatureRequest_<cuid>",
    "publicKey": "<hex public key from messages>",
    "message": "<hex message from messages>",
    "signature": "<hex signature>"
  }
}
```

- `publicKey` and `message` must match an entry in `messages` exactly. Send one call
  per entry you sign.
- The caller needs sign permission on the vault. Signing does not require the same
  identity that created the request.

### Produce a signature

The entry's `curve` names the signature scheme. Every value is hexadecimal: decode
`message` to bytes before signing, and hex-encode the signature you send.

| `curve`     | Sign                                                             | `signature` value                                   |
| ----------- | ---------------------------------------------------------------- | --------------------------------------------------- |
| `BLS12_381` | The message bytes, with the augmented scheme (`AugSchemeMPL`)    | The 96-byte G2 signature                            |
| `SECP256R1` | The message bytes, with ECDSA over P-256 and SHA-256             | The 64-byte `r` and `s` values, or a DER signature  |
| `WEBAUTHN`  | A WebAuthn assertion whose challenge is derived from the message | A JSON document, UTF-8 encoded and then hex-encoded |

For `BLS12_381`, sign exactly the bytes of `message`. It already contains the coin
and network data that Chia's `AGG_SIG` conditions require, so do not hash it or
append anything. `AugSchemeMPL.sign` in the `chia_rs` or `blspy` libraries produces
the expected signature.

For `SECP256R1`, the key is a 33-byte compressed public key. Sign the message with
the SHA-256 digest, as standard ECDSA libraries do by default; do not hash it
yourself first.

For `WEBAUTHN`, request an assertion from the authenticator with the challenge set
to the SHA-256 digest of the message bytes. If the message is already 32 bytes, treat
it as a digest and use it as the challenge unchanged. Pass the challenge as bytes,
for example to `navigator.credentials.get`; the browser writes it into
`clientDataJSON` as base64url, which Cloud Wallet checks. Then send this JSON
document, UTF-8 encoded and hex-encoded, as `signature`:

```json
{
  "authenticatorData": "<hex authenticatorData from the assertion>",
  "clientDataJSON": "<hex clientDataJSON from the assertion>",
  "signature": "<hex signature from the assertion>"
}
```

The `Curve` enum also lists `SECP256K1`, but vaults cannot spend with secp256k1 keys.

A signature that does not verify fails with `BAD_USER_INPUT` and a message such as
`bls12-381 signature does not verify`.

API-key clients can subscribe to
[`signatureRequestCreated`](/docs#subscription-signatureRequestCreated) and
[`signatureRequestUpdated`](/docs#subscription-signatureRequestUpdated) to learn when
a request needs a signature or changes status.
[`resendSignatureRequest`](/docs#mutation-resendSignatureRequest) notifies the signers again
while the request is `UNSIGNED`.

### Submission and `autoSubmit`

`autoSubmit` decides what happens when the last required signature arrives:

| `autoSubmit` | After the last signature                                                                                 |
| ------------ | -------------------------------------------------------------------------------------------------------- |
| `true`       | Cloud Wallet submits the spend in the same `signSignatureRequest` call. The request moves to `SUBMITTED` |
| `false`      | The request stays `SIGNED` until you call `submitSignatureRequest`                                       |

Each mutation that creates a request documents its `autoSubmit` default, and the
defaults differ; see the [conventions section](/docs#introduction-item-2). The request's `autoSubmit` field
records the value in effect.

With `autoSubmit: false`, submit the fully signed request yourself:

```graphql
mutation SubmitSignatureRequest($input: SubmitSignatureRequestInput!) {
  submitSignatureRequest(input: $input) {
    signatureRequest {
      id
      status
      transaction {
        id
        state
      }
    }
  }
}
```

```json
{
  "input": {
    "signatureRequestId": "SignatureRequest_<cuid>"
  }
}
```

Submitting a request in any status other than `SIGNED` fails with
`Signature request is not in the correct state to be submitted`. A request whose
time lock has passed fails with `This transaction has expired and is no longer valid.`

### When automatic submission fails

If the final signature is accepted but automatic submission fails, the
`signSignatureRequest` call returns an `AUTO_SUBMIT_FAILED` error. The signature is
still stored and the request is `SIGNED`. Check `extensions.retryable` on the error:

- `true`: the failure may be temporary. Retry with
  [`submitSignatureRequest`](/docs#mutation-submitSignatureRequest).
- `false`: the network rejected the spend. Cloud Wallet cancels the request, and the
  transaction moves to `INVALID`. Create a new transaction.

## Offers

An offer is a partially signed trade that anyone holding it can accept on chain. It
trades XCH, CATs, and NFTs without a counterparty or exchange in between. Offers
travel as bech32m strings beginning with `offer1`.

### Create an offer

[`createOffer`](/docs#mutation-createOffer) describes what the vault gives and what it wants in return.
Amounts are in mojos, and an omitted `assetHash` means XCH. This input offers 1 XCH
for 100 units of a CAT:

```graphql
mutation CreateOffer($input: CreateOfferInput!) {
  createOffer(input: $input) {
    signatureRequest {
      id
      status
    }
  }
}
```

```json
{
  "input": {
    "walletId": "Wallet_<cuid>",
    "offered": [{ "amount": "1000000000000" }],
    "requested": [{ "assetHash": "<64-character-cat-tail-hash>", "amount": "100000" }],
    "fee": "0",
    "expiresAt": "2026-12-31T00:00:00Z"
  }
}
```

To trade NFTs, list their 64-character hexadecimal launcher IDs in `offeredNftIds` or
`requestedNftIds`. `offered` and `requested` are still required; pass `[]` for a side
that has no XCH or CATs.

The mutation returns a signature request, not the offer string. Sign it as described
in the [signature requests section](/docs#introduction-item-4). When the last signature arrives, Cloud Wallet
records the offer and moves the request to `SUBMITTED`. Nothing is sent to the
network: a created offer stays off chain until someone takes it or you cancel it on
chain.

`fee` is included in the offer's own spend, so it is paid when the offer settles,
not when you create it. `splitInputCoinsFee` pays for the coin split described next.

With `splitInputCoins: true`, Cloud Wallet splits the vault's coins first when that
is needed for the offer to reserve only the offered amount. The signature request
then covers that coin split, which is sent to the network, and `autoSubmit` works as
it does for a
transfer: with `false`, the request stays `SIGNED` until you call
`submitSignatureRequest`. The offer is recorded when the request is signed in both
cases.

Read the offer string from the request's transaction once it is signed:

```graphql
query CreatedOffer($id: ID!) {
  signatureRequest(id: $id) {
    status
    transaction {
      offer {
        id
        offerId
        bech32
        state
        settlementType
      }
    }
  }
}
```

Share `bech32` with the counterparty. Keep `offerId`, which `cancelOffer` needs, and
`id`, which reads the offer again with [`walletOffer`](/docs#query-walletOffer). The vault's offered coins stay reserved, and
are excluded from its spendable balance, until the offer settles, expires, or is
cancelled.

### Take an offer

Taking an offer string you received has two steps. First import it into the vault
that will take it with [`addOffer`](/docs#mutation-addOffer), which returns the offer's details
without committing to anything:

```graphql
mutation ImportOffer($input: AddOfferInput!) {
  addOffer(input: $input) {
    offer {
      id
      offerId
      state
      expiresAt
      assets(first: 10) {
        totalCount
      }
    }
  }
}
```

```json
{
  "input": {
    "walletId": "Wallet_<cuid>",
    "offer": "offer1..."
  }
}
```

Inspect the imported offer before accepting it. Then pass its `offerId`, in
`Offer_<hex>` format, to [`takeOffer`](/docs#mutation-takeOffer):

```graphql
mutation TakeOffer($input: TakeOfferInput!) {
  takeOffer(input: $input) {
    signatureRequest {
      id
      status
    }
  }
}
```

```json
{
  "input": {
    "walletId": "Wallet_<cuid>",
    "offerId": "Offer_<hex>",
    "fee": "0",
    "autoSubmit": true
  }
}
```

`takeOffer` defaults to `autoSubmit: false`, so without the explicit `true` above
the request waits in `SIGNED` for [`submitSignatureRequest`](/docs#mutation-submitSignatureRequest). An offer in
an outdated format fails with `UNSUPPORTED_OFFER`.

### Cancel an offer

[`cancelOffer`](/docs#mutation-cancelOffer) cancels an offer the vault created, in one of two ways:

| Option                 | Effect                                                                                                   | Returns                  |
| ---------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------ |
| On chain (default)     | Spends the offered coins so the offer can never be taken. Costs a transaction and the `fee` you set      | A signature request      |
| `cancelOffChain: true` | Marks the offer cancelled in Cloud Wallet. **Anyone who already has the offer string can still take it** | `null` signature request |

An off-chain cancellation also releases the reservation, so the offered coins count
as spendable again. Until those coins are spent, the offer can still be taken. When
a later transaction spends them, the offer can no longer settle.

Cancel on chain whenever the offer string has left your system. An on-chain
cancellation defaults to `autoSubmit: false`.

```graphql
mutation CancelOffer($input: CancelOfferInput!) {
  cancelOffer(input: $input) {
    signatureRequest {
      id
      status
    }
  }
}
```

```json
{
  "input": {
    "walletId": "Wallet_<cuid>",
    "offerId": "Offer_<hex>",
    "fee": "0",
    "autoSubmit": true
  }
}
```

### Offer states

An offer has two status fields. Both are strings with the values below.

`state` describes the offer on chain, and is the same for every vault that holds it:

| State     | Meaning                                                                    |
| --------- | -------------------------------------------------------------------------- |
| `PENDING` | The offer's input coins are still being prepared                           |
| `OPEN`    | The offer can be taken                                                     |
| `MEMPOOL` | A take or cancellation is waiting to be included in a block                |
| `SETTLED` | The offer's coins were spent on chain; `settlementType` says how           |
| `RE_ORG`  | A chain reorganization removed the settling block                          |
| `EXPIRED` | `expiresAt` passed before the offer was taken                              |
| `INVALID` | The offer can no longer be taken, for example because its coins were spent |

`settlementType` describes the outcome for your vault:

| Settlement type        | Meaning                                                                              |
| ---------------------- | ------------------------------------------------------------------------------------ |
| `UNSIGNED`             | Your vault's take is not fully signed                                                |
| `PENDING_CONFIRMATION` | Your vault's take was submitted and is waiting to confirm                            |
| `PENDING_CANCELLATION` | Your vault's on-chain cancellation is waiting to confirm                             |
| `CONFIRMED`            | The trade completed: your vault's offer was taken, or your vault's take settled it   |
| `CANCELLED`            | The creator cancelled the offer on chain, or your vault cancelled it in Cloud Wallet |
| `TAKEN_BY_OTHERS`      | Your vault imported or tried to take the offer, and another party took it first      |

`settlementType` is `null` while your vault's own offer is open, and on an imported
offer your vault has not acted on. List a
vault's offers with the `offers` connection on [`Wallet`](/docs#definition-Wallet), filtered by `states`
or `settlementTypes`, and read one with [`walletOffer`](/docs#query-walletOffer).
API-key clients can subscribe to [`offerUpdated`](/docs#subscription-offerUpdated).

## NFTs

### Mint NFTs

[`bulkMintNfts`](/docs#mutation-bulkMintNfts) requires `nft.create` permission for the source vault. The
vault needs one XCH mojo per NFT plus the fee. Hashes are SHA-256 values encoded as 64
hexadecimal characters. URI arrays must contain a valid first URL.

```graphql
mutation MintNfts($input: BulkMintNftsInput!) {
  bulkMintNfts(input: $input) {
    nftIds
    airdropId
    signatureRequest {
      id
      status
    }
  }
}
```

```json
{
  "input": {
    "walletId": "Wallet_<cuid>",
    "fee": "0",
    "autoSubmit": true,
    "mints": [
      {
        "dataHash": "<64-character-sha256>",
        "dataUris": ["https://example.com/nft.png"],
        "metadataHash": "<64-character-sha256>",
        "metadataUris": ["https://example.com/metadata.json"],
        "royaltyBasisPoints": 0
      }
    ]
  }
}
```

Omit `ownerAddress` to mint to the source vault. Omit edition fields for a one-of-one
NFT. The mutation returns deterministic NFT launcher IDs and a signature request; it
does not bypass the vault's signer policy. The signature request then follows the
lifecycle in the [signature requests section](/docs#introduction-item-4).

### Transfer NFTs

[`transferNfts`](/docs#mutation-transferNfts) sends one or more NFTs from a vault, each
to its own address. It requires `transfer.create` permission for the vault and
defaults to `autoSubmit: false`. Identify each NFT by its launcher ID, the
64-character hexadecimal `launcherId` on the vault's `nfts` connection.

```graphql
mutation TransferNfts($input: TransferNftsInput!) {
  transferNfts(input: $input) {
    signatureRequest {
      id
      status
    }
  }
}
```

```json
{
  "input": {
    "walletId": "Wallet_<cuid>",
    "transfers": [{ "launcherId": "<64-character-launcher-id>", "address": "xch1..." }],
    "fee": "0",
    "autoSubmit": true
  }
}
```
