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

# Quickstart: first wallet and transaction

> Create an MPC wallet in the sandbox, receive test funds, and send a governed withdrawal.

This guide creates a wallet whose key is generated as shares across the signing nodes, attaches an Ethereum Sepolia asset to it, and sends a small withdrawal through policy, threshold signing and broadcast. It takes about fifteen minutes; most of that is waiting for testnet confirmations.

## Before you start

<Steps>
  <Step title="Request sandbox access">
    Email [hello@blockops.network](mailto:hello@blockops.network) with your company name and the address you will integrate from. You receive a **workspace ID**, an **API key** and an **API secret** for the sandbox at `https://wallet-sandbox.blockops.network`. The sandbox runs on Ethereum Sepolia; nothing you do in it touches real funds.
  </Step>

  <Step title="Have a Sepolia faucet ready">
    You will need a small amount of Sepolia ETH in the wallet you create. Any public Sepolia faucet works.
  </Step>
</Steps>

## Sign every request

Each request carries three headers: `ACCESS-API-KEY`, `ACCESS-TIMESTAMP` and `ACCESS-SIGN`. The signature is an HMAC-SHA256 over `method=<METHOD>&path=<PATH>&timestamp=<TIMESTAMP>&body=<RAW BODY>`, keyed with the API secret and hex-encoded. Timestamps older or newer than five minutes are rejected.

Set `API_KEY`, `API_SECRET` and `WORKSPACE_ID` in your environment to the values you received. In cURL, each step computes the signature with `openssl` and passes it to a plain `curl` call. In TypeScript and Go, the `req` function below does the same and is used by every step.

<CodeGroup>
  ```bash cURL theme={null}
  # Every call below follows this recipe: compute the signature, then send it.
  TS=$(date +%s)
  SIG=$(printf '%s' "method=<METHOD>&path=<PATH>&timestamp=$TS&body=<RAW BODY>" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

  curl -X <METHOD> https://wallet-sandbox.blockops.network<PATH> \
    -H "Content-Type: application/json" \
    -H "ACCESS-API-KEY: $API_KEY" \
    -H "ACCESS-TIMESTAMP: $TS" \
    -H "ACCESS-SIGN: $SIG" \
    -d '<RAW BODY>'
  ```

  ```typescript TypeScript theme={null}
  import { createHmac } from "node:crypto";

  const BASE = "https://wallet-sandbox.blockops.network";
  const { API_KEY, API_SECRET, WORKSPACE_ID } = process.env;

  export async function req(method: string, path: string, body?: unknown, headers: Record<string, string> = {}) {
    const raw = body === undefined ? "" : JSON.stringify(body);
    const ts = Math.floor(Date.now() / 1000).toString();
    const sig = createHmac("sha256", API_SECRET!)
      .update(`method=${method}&path=${path}&timestamp=${ts}&body=${raw}`)
      .digest("hex");
    const res = await fetch(BASE + path, {
      method,
      headers: { "Content-Type": "application/json", "ACCESS-API-KEY": API_KEY!, "ACCESS-TIMESTAMP": ts, "ACCESS-SIGN": sig, ...headers },
      body: raw || undefined,
    });
    return res.json();
  }
  ```

  ```go Go theme={null}
  package main

  import (
  	"bytes"
  	"crypto/hmac"
  	"crypto/sha256"
  	"encoding/hex"
  	"fmt"
  	"io"
  	"net/http"
  	"os"
  	"strconv"
  	"time"
  )

  const base = "https://wallet-sandbox.blockops.network"

  func req(method, path string, body []byte, headers map[string]string) ([]byte, error) {
  	ts := strconv.FormatInt(time.Now().Unix(), 10)
  	mac := hmac.New(sha256.New, []byte(os.Getenv("API_SECRET")))
  	fmt.Fprintf(mac, "method=%s&path=%s&timestamp=%s&body=%s", method, path, ts, body)
  	r, _ := http.NewRequest(method, base+path, bytes.NewReader(body))
  	r.Header.Set("Content-Type", "application/json")
  	r.Header.Set("ACCESS-API-KEY", os.Getenv("API_KEY"))
  	r.Header.Set("ACCESS-TIMESTAMP", ts)
  	r.Header.Set("ACCESS-SIGN", hex.EncodeToString(mac.Sum(nil)))
  	for k, v := range headers {
  		r.Header.Set(k, v)
  	}
  	res, err := http.DefaultClient.Do(r)
  	if err != nil {
  		return nil, err
  	}
  	defer res.Body.Close()
  	return io.ReadAll(res.Body)
  }
  ```
</CodeGroup>

For `GET` requests the body is empty, so the canonical string ends with `body=`.

Every response uses the same envelope: `{ "success": true, "message": "...", "code": "...", "data": ... }`. When something is wrong, `success` is `false` and `message` says why.

## 1. Create a wallet

Wallet creation is asynchronous: the API accepts the request, the signing nodes run distributed key generation, and the wallet becomes usable when they finish. Send an idempotency key so a retry cannot create a second wallet.

<CodeGroup>
  ```bash cURL theme={null}
  TS=$(date +%s)
  IDEMPOTENCY_KEY=quickstart-wallet-1   # any unique value you choose per operation
  BODY='{"workspace_id":"'"$WORKSPACE_ID"'","name":"Quickstart treasury"}'
  SIG=$(printf '%s' "method=POST&path=/api/v1/wallets&timestamp=$TS&body=$BODY" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

  curl -X POST https://wallet-sandbox.blockops.network/api/v1/wallets \
    -H "Content-Type: application/json" \
    -H "ACCESS-API-KEY: $API_KEY" \
    -H "ACCESS-TIMESTAMP: $TS" \
    -H "ACCESS-SIGN: $SIG" \
    -H "X-Idempotency-Key: $IDEMPOTENCY_KEY" \
    -d "$BODY"
  ```

  ```typescript TypeScript theme={null}
  const created = await req("POST", "/api/v1/wallets",
    { workspace_id: WORKSPACE_ID, name: "Quickstart treasury" },
    { "X-Idempotency-Key": "quickstart-wallet-1" });
  const walletId = created.data.wallet_id;
  ```

  ```go Go theme={null}
  body := []byte(fmt.Sprintf(`{"workspace_id":%q,"name":"Quickstart treasury"}`, os.Getenv("WORKSPACE_ID")))
  out, err := req("POST", "/api/v1/wallets", body, map[string]string{"X-Idempotency-Key": "quickstart-wallet-1"})
  ```
</CodeGroup>

The response is `202 Accepted`:

```json theme={null}
{
  "success": true,
  "message": "Wallet creation accepted",
  "code": "ACCEPTED",
  "data": {
    "id": "9c1f2f6a-…",
    "wallet_id": "5b7e0d3c-…",
    "workspace_id": "…",
    "name": "Quickstart treasury",
    "wallet_type": "mpc",
    "status": "pending",
    "created_at": "2026-08-29T10:14:02Z"
  }
}
```

Keep `wallet_id`. Poll the creation status until it is `success`:

<CodeGroup>
  ```bash cURL theme={null}
  TS=$(date +%s)
  SIG=$(printf '%s' "method=GET&path=/api/v1/wallets/creation-status/$WALLET_ID&timestamp=$TS&body=" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

  curl -X GET https://wallet-sandbox.blockops.network/api/v1/wallets/creation-status/$WALLET_ID \
    -H "Content-Type: application/json" \
    -H "ACCESS-API-KEY: $API_KEY" \
    -H "ACCESS-TIMESTAMP: $TS" \
    -H "ACCESS-SIGN: $SIG"
  ```

  ```typescript TypeScript theme={null}
  const status = await req("GET", `/api/v1/wallets/creation-status/${walletId}`);
  ```

  ```go Go theme={null}
  out, err := req("GET", "/api/v1/wallets/creation-status/"+walletID, nil, nil)
  ```
</CodeGroup>

Status moves `pending` → `submitted` → `success`. If it ends in `failed`, `status_reason` says why; a failed operation never leaves a partial wallet behind.

<Note>
  Key generation typically takes a few seconds. In production, subscribe to the `wallet.created` webhook instead of polling; see [Webhooks & events](/wallets/webhooks-and-events).
</Note>

## 2. Attach an asset and get a deposit address

A wallet holds a key; an **asset** binds it to a network and token. Look up the Sepolia ETH asset in the catalog, then attach it. The platform derives the address server-side; you never supply a derivation path.

<CodeGroup>
  ```bash cURL theme={null}
  # find the asset whose network is ETHER_SEPOLIA_TESTNET, then attach it:
  TS=$(date +%s)
  SIG=$(printf '%s' "method=GET&path=/api/v1/assets&timestamp=$TS&body=" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

  curl -X GET https://wallet-sandbox.blockops.network/api/v1/assets \
    -H "Content-Type: application/json" \
    -H "ACCESS-API-KEY: $API_KEY" \
    -H "ACCESS-TIMESTAMP: $TS" \
    -H "ACCESS-SIGN: $SIG"

  TS=$(date +%s)
  SIG=$(printf '%s' "method=PUT&path=/api/v1/wallets/$WALLET_ID/asset/$ASSET_ID&timestamp=$TS&body=" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

  curl -X PUT https://wallet-sandbox.blockops.network/api/v1/wallets/$WALLET_ID/asset/$ASSET_ID \
    -H "Content-Type: application/json" \
    -H "ACCESS-API-KEY: $API_KEY" \
    -H "ACCESS-TIMESTAMP: $TS" \
    -H "ACCESS-SIGN: $SIG"
  ```

  ```typescript TypeScript theme={null}
  const assets = await req("GET", "/api/v1/assets");   // find the asset whose network is ETHER_SEPOLIA_TESTNET
  const attached = await req("PUT", `/api/v1/wallets/${walletId}/asset/${assetId}`);
  const address = attached.data.address;
  ```

  ```go Go theme={null}
  out, err := req("GET", "/api/v1/assets", nil, nil) // find the asset whose network is ETHER_SEPOLIA_TESTNET
  out, err = req("PUT", "/api/v1/wallets/"+walletID+"/asset/"+assetID, nil, nil)
  ```
</CodeGroup>

```json theme={null}
{
  "success": true,
  "message": "Asset attached",
  "code": "OK",
  "data": {
    "id": "…",
    "wallet_id": "5b7e0d3c-…",
    "asset_id": "…",
    "network_id": "…",
    "address_type": "evm",
    "address": "0x9aF3…c21D",
    "status": "active"
  }
}
```

Send Sepolia ETH from the faucet to `address`. Deposits are detected by the platform's chain scanner: a `deposit.detected` webhook fires when the transfer is seen, and `balance.updated` when the balance changes.

<CodeGroup>
  ```bash cURL theme={null}
  TS=$(date +%s)
  SIG=$(printf '%s' "method=GET&path=/api/v1/wallets/$WALLET_ID/balance/$ASSET_ID&timestamp=$TS&body=" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

  curl -X GET https://wallet-sandbox.blockops.network/api/v1/wallets/$WALLET_ID/balance/$ASSET_ID \
    -H "Content-Type: application/json" \
    -H "ACCESS-API-KEY: $API_KEY" \
    -H "ACCESS-TIMESTAMP: $TS" \
    -H "ACCESS-SIGN: $SIG"
  ```

  ```typescript TypeScript theme={null}
  const balance = await req("GET", `/api/v1/wallets/${walletId}/balance/${assetId}`);
  ```

  ```go Go theme={null}
  out, err := req("GET", "/api/v1/wallets/"+walletID+"/balance/"+assetID, nil, nil)
  ```
</CodeGroup>

## 3. Request a withdrawal

A withdrawal is a request, not an instruction. The platform validates it, evaluates the workspace's policies, holds the amount against the balance, plans the transaction, has the signing nodes produce a threshold signature, broadcasts, and confirms.

<CodeGroup>
  ```bash cURL theme={null}
  TS=$(date +%s)
  IDEMPOTENCY_KEY=quickstart-withdrawal-1
  BODY='{"asset_id":"'"$ASSET_ID"'","amount":"0.001","recipient_address":"0x1111111111111111111111111111111111111111"}'
  SIG=$(printf '%s' "method=POST&path=/api/v1/wallets/$WALLET_ID/request-withdrawal&timestamp=$TS&body=$BODY" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

  curl -X POST https://wallet-sandbox.blockops.network/api/v1/wallets/$WALLET_ID/request-withdrawal \
    -H "Content-Type: application/json" \
    -H "ACCESS-API-KEY: $API_KEY" \
    -H "ACCESS-TIMESTAMP: $TS" \
    -H "ACCESS-SIGN: $SIG" \
    -H "X-Idempotency-Key: $IDEMPOTENCY_KEY" \
    -d "$BODY"
  ```

  ```typescript TypeScript theme={null}
  const withdrawal = await req("POST", `/api/v1/wallets/${walletId}/request-withdrawal`,
    { asset_id: assetId, amount: "0.001", recipient_address: "0x1111111111111111111111111111111111111111" },
    { "X-Idempotency-Key": "quickstart-withdrawal-1" });
  const withdrawalId = withdrawal.data.withdrawal.withdrawal_id;
  ```

  ```go Go theme={null}
  body := []byte(fmt.Sprintf(`{"asset_id":%q,"amount":"0.001","recipient_address":"0x1111111111111111111111111111111111111111"}`, assetID))
  out, err := req("POST", "/api/v1/wallets/"+walletID+"/request-withdrawal", body, map[string]string{"X-Idempotency-Key": "quickstart-withdrawal-1"})
  ```
</CodeGroup>

```json theme={null}
{
  "success": true,
  "message": "Withdrawal accepted",
  "code": "ACCEPTED",
  "data": {
    "withdrawal": {
      "id": "…",
      "withdrawal_id": "d4a2…",
      "wallet_id": "5b7e0d3c-…",
      "asset_id": "…",
      "amount": "0.001",
      "recipient_address": "0x1111…1111",
      "status": "pending"
    },
    "signing_request": {
      "id": "…",
      "status": "pending"
    }
  }
}
```

A fresh sandbox workspace has no approval rules, so the request proceeds straight to signing. If a policy had required approval, `status` would stay `pending` until an approver acted in the console; the API call itself never bypasses that.

Poll the withdrawal until it reaches a terminal status:

<CodeGroup>
  ```bash cURL theme={null}
  TS=$(date +%s)
  SIG=$(printf '%s' "method=GET&path=/api/v1/withdrawals/$WITHDRAWAL_ID&timestamp=$TS&body=" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

  curl -X GET https://wallet-sandbox.blockops.network/api/v1/withdrawals/$WITHDRAWAL_ID \
    -H "Content-Type: application/json" \
    -H "ACCESS-API-KEY: $API_KEY" \
    -H "ACCESS-TIMESTAMP: $TS" \
    -H "ACCESS-SIGN: $SIG"
  ```

  ```typescript TypeScript theme={null}
  const w = await req("GET", `/api/v1/withdrawals/${withdrawalId}`);
  ```

  ```go Go theme={null}
  out, err := req("GET", "/api/v1/withdrawals/"+withdrawalID, nil, nil)
  ```
</CodeGroup>

`submitted` means the signed transaction is on the network; `success` means it has reached the configured confirmation depth, and `tx_hash`, `block_number` and `gas_used` are filled in. Look the hash up on a Sepolia explorer to see the transfer.

## What just happened

* The wallet's private key never existed in one place. Each signing node holds a share, and signing needed a threshold of them to cooperate.
* Your API key could only do what its scopes allow. The sandbox key you received carries `wallets:create`, `wallets:read`, `assets:attach`, `balances:read`, `withdrawals:create` and `withdrawals:read`.
* Every step (creation, attachment, withdrawal request, signing, broadcast) is recorded and available through [Reporting & audit](/wallets/reporting-and-audit).

## Where to next

<CardGroup cols={2}>
  <Card title="Add a policy" icon="scale-balanced" href="/wallets/policies-and-approvals">
    Require approval above a threshold, whitelist destinations, set daily limits.
  </Card>

  <Card title="Receive webhooks" icon="bell" href="/wallets/webhooks-and-events">
    Replace polling with signed event deliveries.
  </Card>

  <Card title="API reference" icon="code" href="/api-reference/introduction">
    Every endpoint, request and response.
  </Card>

  <Card title="Go to production" icon="rocket" href="/overview/deployment-models">
    Choose managed, hybrid or self-hosted signing.
  </Card>
</CardGroup>
