> ## Documentation Index
> Fetch the complete documentation index at: https://blank.build/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Operations and Transaction Intents

> Track asynchronous Blank operations, and sign and submit wallet-authorized transaction intents through API v2.

# Operations and Transaction Intents

Two resources cover everything asynchronous or wallet-signed in API v2:

* An **operation** is long-running server work you poll for a terminal state.
* A **transaction intent** is a Solana transaction that Blank prepared for a specific wallet, which that wallet must sign before Blank will broadcast it.

<Warning>
  An API key never replaces a wallet signature. It authorizes preparing and
  submitting a transaction; the pinned wallet still signs the exact prepared
  message. Blank verifies the message, the required signer, the blockhash
  window, the intent policy, and the resource version before broadcast.
</Warning>

## Operations

| Operation      | HTTP                            | SDK                                 | Scope             |
| -------------- | ------------------------------- | ----------------------------------- | ----------------- |
| `getOperation` | `GET /operations/{operationId}` | `blank.operations.get(operationId)` | `operations:read` |

Operations are account-scoped: a key can only read operations owned by its own Blank account. Unknown or foreign IDs return `404 operation_not_found`.

### Fields

| Field         | Type                    | Notes                                                             |
| ------------- | ----------------------- | ----------------------------------------------------------------- |
| `id`          | uuid                    |                                                                   |
| `kind`        | string                  | Lowercase identifier for the work being performed                 |
| `status`      | enum                    | `queued`, `running`, `succeeded`, `failed`, `cancelled`           |
| `progress`    | object \| absent        | `{ completed, total, unit? }` when the operation reports progress |
| `createdAt`   | RFC 3339                |                                                                   |
| `startedAt`   | RFC 3339 \| null        |                                                                   |
| `completedAt` | RFC 3339 \| null        |                                                                   |
| `resultUrl`   | url \| null             | Populated when the result is retrievable                          |
| `error`       | Problem Details \| null | Populated on `failed`                                             |

`queued` and `running` responses carry `Retry-After: 2`. Honour it instead of tight polling.

### Polling

```ts theme={null}
async function waitForOperation(operationId: string, deadlineMs = 300_000) {
  const deadline = Date.now() + deadlineMs;

  while (Date.now() < deadline) {
    const { data, metadata } = await blank.operations.get(operationId);

    if (data.status !== "queued" && data.status !== "running") return data;

    const waitSeconds = metadata.retryAfterSeconds ?? 2;
    await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1_000));
  }

  throw new Error(`Operation ${operationId} did not settle in time`);
}
```

A `failed` operation carries the full Problem Details in `error`, including the `code` and the `requestId` of the failure. Log both.

Rather than polling at all, subscribe to `build.blank.operation.failed.v1` and the completion events you care about. See [Webhooks](/docs/for-developers/webhooks).

## Transaction intents

| Operation                 | HTTP                                               | SDK                                                         | Scope                | Success |
| ------------------------- | -------------------------------------------------- | ----------------------------------------------------------- | -------------------- | ------- |
| `getTransactionIntent`    | `GET /transaction-intents/{intentId}`              | `blank.transactionIntents.get(intentId)`                    | `operations:read`    | `200`   |
| `submitTransactionIntent` | `POST /transaction-intents/{intentId}/submissions` | `blank.transactionIntents.submit(intentId, input, options)` | `transactions:write` | `202`   |

Intents are account-scoped in the same way. The public contract exposes **retrieval and signed submission**; an intent is prepared by the Blank flow that needs a wallet signature, and you receive its `intentId` from that flow.

### Fields

| Field                                       | Type                       | Notes                                                     |
| ------------------------------------------- | -------------------------- | --------------------------------------------------------- |
| `id`                                        | uuid                       |                                                           |
| `kind`                                      | string                     | What the transaction does                                 |
| `status`                                    | enum                       | `prepared`, `submitted`, `confirmed`, `expired`, `failed` |
| `expectedSigner`                            | base58                     | The only wallet whose signature is accepted               |
| `serializedTransaction`                     | string                     | Base64 transaction to sign, byte-for-byte                 |
| `recentBlockhash`                           | string \| null             |                                                           |
| `lastValidBlockHeight`                      | string \| null             | Unsigned integer string                                   |
| `policy`                                    | object                     | Server-side constraints enforced at submission            |
| `transactionSignature`                      | string \| null             | Populated once broadcast                                  |
| `error`                                     | `{ code, detail }` \| null | Safe failure summary                                      |
| `version`                                   | integer                    | Required in `If-Match` on submission                      |
| `expiresAt`                                 | RFC 3339                   | After this, the intent cannot be submitted                |
| `createdAt` / `submittedAt` / `completedAt` | RFC 3339 \| null           |                                                           |

### Lifecycle

1. **`prepared`** — Blank has built the transaction and is waiting for a signature.
2. **`submitted`** — the signature verified and the transaction is durably queued for broadcast. The response carries `Retry-After: 2`.
3. **`confirmed`** — the transaction landed. `transactionSignature` is populated.
4. **`expired`** — the blockhash window closed before a valid submission arrived.
5. **`failed`** — broadcast or confirmation failed. `error` explains why.

### Signing and submitting

```ts theme={null}
const intent = await blank.transactionIntents.get(intentId);

if (intent.data.status !== "prepared") {
  throw new Error(`Intent is ${intent.data.status}, not prepared`);
}

// Inspect the transaction before signing, then sign the exact bytes.
const signedTransaction = await signBase64Transaction(
  intent.data.serializedTransaction,
  intent.data.expectedSigner
);

const submitted = await blank.transactionIntents.submit(
  intent.data.id,
  { signedTransaction, version: intent.data.version },
  { idempotencyKey: `intent-${intent.data.id}-submit` }
);

console.log(submitted.data.status); // "submitted"
```

<Warning>
  Sign the prepared message byte-for-byte. Do not let a wallet rewrite the fee
  payer, blockhash, compute-budget instructions, or lookup tables. Any change to
  the message returns `422 transaction_intent_tampered`. And never sign a
  transaction your application has not independently inspected.
</Warning>

Submission requires both concurrency headers, which the SDK sets from the arguments you pass:

* `If-Match: "<version>"` — the intent version you read
* `Idempotency-Key` — a durable key you should generate and persist yourself

The `202` response includes `Location` pointing at the intent, `ETag` with the new version, and `Retry-After: 2`.

Raw HTTP:

```bash theme={null}
curl -s -X POST \
  "https://api.blank.build/api/v2/transaction-intents/$INTENT_ID/submissions" \
  -H "Authorization: Bearer $BLANK_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: intent-$INTENT_ID-submit" \
  -H 'If-Match: "1"' \
  -d '{"signedTransaction":"'"$SIGNED_TRANSACTION_BASE64"'"}'
```

### Reconciling an uncertain submission

A `202` means the transaction is durably queued, not that it landed. Never assume an outcome from a dropped connection.

1. Retry the submission with the **same** `Idempotency-Key`. A completed attempt replays with `Idempotent-Replayed: true`; an in-flight attempt returns `409 idempotency_request_in_progress` with `Retry-After: 1`.
2. Poll `getTransactionIntent` until `status` reaches `confirmed`, `failed`, or `expired`, honouring `Retry-After`.
3. On `confirmed`, record `transactionSignature`. On `failed`, read `error.code`. On `expired`, request a fresh intent — a signature cannot be reused across intents.

`TransactionIntent.error.code` is a closed contract: `solana_transaction_failed`, `transaction_intent_invalid_state`, `transaction_intent_expired`, `transaction_intent_broadcast_exhausted`, `solana_blockhash_expired`, or `solana_signature_mismatch`. See [Transaction-intent result codes](/docs/reference/errors#transaction-intent-result-codes) for the action associated with each one.

```ts theme={null}
async function reconcileIntent(intentId: string, deadlineMs = 120_000) {
  const deadline = Date.now() + deadlineMs;

  while (Date.now() < deadline) {
    const { data, metadata } = await blank.transactionIntents.get(intentId);
    if (data.status === "confirmed") return data.transactionSignature;
    if (data.status === "failed" || data.status === "expired") {
      throw new Error(`Intent ${intentId} ${data.status}: ${data.error?.code}`);
    }
    await new Promise((r) =>
      setTimeout(r, (metadata.retryAfterSeconds ?? 2) * 1_000)
    );
  }

  throw new Error(`Intent ${intentId} did not settle in time`);
}
```

### Submission errors

| Status | Code                                   | Meaning                                              | Fix                                                        |
| ------ | -------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------------- |
| 404    | `transaction_intent_not_found`         | Unknown intent, or owned by another account          | Check the ID and the key's account                         |
| 409    | `transaction_intent_not_prepared`      | The intent is no longer awaiting a signature         | Re-read it; it may already be submitted                    |
| 409    | `transaction_intent_expired`           | The blockhash window closed                          | Request a fresh intent and sign again                      |
| 412    | `transaction_intent_version_conflict`  | `If-Match` did not match the current version         | Re-read the intent and resubmit with its version           |
| 400    | `precondition_invalid`                 | `If-Match` is not a strong numeric ETag              | Send the exact ETag from the latest intent read            |
| 428    | `precondition_required`                | `If-Match` is missing                                | Read the intent and send its ETag                          |
| 422    | `transaction_intent_tampered`          | The signed message differs from the prepared message | Sign the exact bytes; disable wallet transaction rewriting |
| 422    | `transaction_intent_wrong_signer`      | Signed by a wallet other than `expectedSigner`       | Sign with the wallet pinned to the API key                 |
| 422    | `transaction_intent_invalid_signature` | The signature failed verification                    | Re-sign the prepared transaction                           |

Shared errors also apply: `401 invalid_api_key`, `403 insufficient_scope`, `409 idempotency_key_reused`, `409 idempotency_request_in_progress`, `429 rate_limit_exceeded`. See [Errors](/docs/reference/errors) and [Conventions](/docs/for-developers/conventions).
