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

# Wallets and payment handles

> Connected wallets, payment handles, public payment acceptance and the payer-facing display profile.

A connected wallet is an address the account holds on one chain, where it can settle and send from. A payment handle is a permanent name that resolves to the account, so a sender never needs its address.

## When to use them

| Resource | Use it to | Set with |
| - | - | - |
| `ConnectedWallet` | Give the account a settlement wallet and a payment source | `POST /v2/accounts/{accountId}/wallets` |
| `PaymentHandle` | Let a send address the account by name, and give it a public payment page | `PUT /v2/accounts/{accountId}/payment-handle` |
| `paymentAcceptance` | Turn the account's public payment page on or off | `PUT /v2/accounts/{accountId}/payment-acceptance` |
| `displayProfile` | Set the name and logo payers see on the account's payments | `PUT /v2/accounts/{accountId}/display-profile` |

Wallets and a handle can also be passed as `wallets` and `handle` on `POST /v2/accounts`, which provisions them in the same transaction. See [Universal Payment Account](/universal-payment-account#create-one).

## The connected wallet

| Field | What it holds |
| - | - |
| `id` | `wallet_` prefix |
| `accountId` | The owning account |
| `kind` | `evm`, `tron`, `solana` or `movement` |
| `chainId` | Arbitrum `42161`, Ethereum `1`, Base `8453`, Polygon `137`, BNB Smart Chain `56`, Avalanche `43114`, Robinhood Chain `4663`, Tempo `4217`, Solana `10103`, Movement `10002`, Tron `728126428` |
| `address` | EVM addresses are stored lowercase; other families keep their casing |
| `label` | Your label. Linking without one stores `Connected wallet` |
| `status` | `active` or `disabled` |
| `ownershipVerificationStatus` | `unverified` or `verified`. See [Ownership verification](#ownership-verification) |
| `ownershipVerificationMethod`, `ownershipVerifiedAt` | How and when ownership was proven, or null |
| `settlementProfileId` | The active settlement profile id when this wallet is the account's settlement wallet; otherwise null |
| `createdAt`, `updatedAt` | Timestamps |

There is no wallet list endpoint: `GET /v2/accounts/{accountId}` returns the account's wallets as `connectedWallets`.

## Link wallets

```bash theme={null}
curl -X POST https://staging-api-v2.stableyard.fi/v2/accounts/acct_123/wallets \
  -u "$APP_ID:$APP_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "wallets": [
      { "chainId": 8453, "address": "0x1111111111111111111111111111111111111111", "label": "Primary wallet", "isPreferredSettlement": true }
    ]
  }'
```

```json theme={null}
[
  {
    "id": "wallet_123",
    "accountId": "acct_123",
    "kind": "evm",
    "chainId": 8453,
    "address": "0x1111111111111111111111111111111111111111",
    "label": "Primary wallet",
    "status": "active",
    "ownershipVerificationStatus": "unverified",
    "ownershipVerificationMethod": null,
    "ownershipVerifiedAt": null,
    "settlementProfileId": "settle_123",
    "createdAt": "2026-10-01T12:00:00.000Z",
    "updatedAt": "2026-10-01T12:00:00.000Z"
  }
]
```

| Rule | Detail |
| - | - |
| Request | `wallets` holds one or more `{ chainId, address, label?, isPreferredSettlement? }`. The address family must match the chain. Bitcoin is deposit-only and cannot be linked |
| Linking again | The same chain and address under the same account returns the same `wallet_` id, reactivated, with the label from this request |
| Preferred settlement | On an account with no settlement profile, the first settlement-supported wallet becomes the settlement wallet. Otherwise settlement changes only for the one wallet marked `isPreferredSettlement: true`; marking two is rejected |
| Settlement-supported chains | Every linkable chain except Tron. A Tron wallet links but cannot be the settlement wallet |
| Payment source | The wallet that becomes the settlement wallet is also set as the account's payment source, replacing the previous one |
| Destinations | Each linked wallet appears as a `crypto_wallet` entry in `GET /v2/accounts/{accountId}/settlement-destinations` |

To change only the payment source afterwards, use `PUT /v2/accounts/{accountId}/payment-source`; see [Sending payments](/payments/sending-payments#set-a-payment-source-first). To pick a different settlement destination, see [Settlement destinations](/settlement/destinations#set-the-settlement-profile).

## Ownership verification

| `ownershipVerificationStatus` | Meaning |
| - | - |
| `unverified` | Every linked wallet starts here. The address was supplied; nobody has proven control of it |
| `verified` | Stableyard recorded a successful ownership proof. `ownershipVerificationMethod` and `ownershipVerifiedAt` say how and when |

Linking a wallet never verifies it, and the Partner API has no call that submits a proof. Partner-managed KYC treats a wallet address as verified information only when the wallet is `verified`. Show `unverified` to your users as exactly that.

## The payment handle

| Field | What it holds |
| - | - |
| `id` | `handle_` prefix |
| `accountId` | The account it resolves to |
| `namespace` | Your app's namespace |
| `handle` | The stored, normalized handle |
| `status` | `active` or `disabled` |
| `createdAt`, `updatedAt` | Timestamps |

The full address is `handle@namespace`, for example `alice@partner`.

## Claim a handle

```bash theme={null}
curl https://staging-api-v2.stableyard.fi/v2/payment-handles/alice/availability \
  -u "$APP_ID:$APP_SECRET"
```

```json theme={null}
{ "namespace": "partner", "handle": "alice", "available": true, "status": "available" }
```

```bash theme={null}
curl -X PUT https://staging-api-v2.stableyard.fi/v2/accounts/acct_123/payment-handle \
  -u "$APP_ID:$APP_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ "handle": "alice" }'
```

```json theme={null}
{
  "id": "handle_123",
  "accountId": "acct_123",
  "namespace": "partner",
  "handle": "alice",
  "status": "active",
  "createdAt": "2026-10-01T12:00:00.000Z",
  "updatedAt": "2026-10-01T12:00:00.000Z"
}
```

<Warning>
  **A handle is permanent.** Each account gets one, it cannot be changed after assignment, and it is never released for reuse. Whatever your customer picks is final.
</Warning>

| Rule | Detail |
| - | - |
| Format | Lowercased, with characters other than `a-z`, `0-9`, `_`, `.` and `-` replaced by `-` and leading or trailing `-` trimmed. 3 to 40 characters after that. Reserved names are refused. Store the `handle` from the response, not your input |
| Namespace | A bare `alice` uses your app's namespace. `alice@namespace` is accepted only when the namespace is your app's |
| Replay | Sending the same handle again returns the existing handle |
| Change | A different handle on an account that has one returns `409 conflict` with `details.code: "payment_handle_immutable"` |

## Resolve a handle

| Where the handle is used | Bare `alice` | Qualified `alice@namespace` |
| - | - | - |
| `GET /v2/payment-handles/{handle}` | Resolves in your namespace | Your own namespace only; another namespace is refused |
| `GET /v2/payment-handles/{handle}/availability` | Checks your namespace | Your own namespace only |
| `destination.type: "payment_handle"` on a send | Resolves in your namespace | Any namespace, including another partner's account |
| The public payment page | Not accepted | Required |

`GET /v2/payment-handles/{handle}` returns `{ accountId, paymentHandle }`, where `accountId` is the account to use on account-scoped calls. An unknown handle returns `404 not_found`.

You can pay another partner's qualified handle but cannot look it up first, so confirm it with your customer before sending. The resolved account comes back on the payment as `destination.accountId`; see [Stablecoin transfers](/payments/stablecoin-transfers#read-both-sides-of-a-cross-partner-transfer).

## Public payment acceptance

`paymentAcceptance` on the account is `{ publicReceiveEnabled, version }`. `publicReceiveEnabled` starts `false`. When it is `true`, payers can open the account's public payment page by its qualified handle and create a receive payment to it from a browser.

```bash theme={null}
curl -X PUT https://staging-api-v2.stableyard.fi/v2/accounts/acct_123/payment-acceptance \
  -u "$APP_ID:$APP_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ "publicReceiveEnabled": true, "expectedVersion": 1, "reason": "Merchant enabled their public payment page" }'
```

| Rule | Detail |
| - | - |
| Body | `publicReceiveEnabled`, `expectedVersion` (the current `paymentAcceptance.version`) and a `reason` of 10 to 500 characters. All three are required |
| Enabling requires | An active qualified payment handle, an eligible preferred settlement destination, and an enabled app-level public Payment policy |
| Concurrency | If `expectedVersion` is stale, the call returns `409 conflict` with `details.expectedVersion` and `details.currentVersion` |
| Effect | Existing payments are unchanged. Each change is audited with your `reason` |
| Response | The account bundle, with the new `paymentAcceptance.version` |

Payers' browsers read the page with `GET /v2/public/payment-recipients/{paymentHandle}` and create the payment with `POST /v2/public/apps/{appId}/payments`. Public send is never allowed.

## The display profile

`displayProfile` on the account is `{ displayName, logoUrl, version }`: the recipient identity payers see, separate from your app's branding. Every payment copies it into `recipientDisplay` at creation, and a later update affects only future payments.

```bash theme={null}
curl -X PUT https://staging-api-v2.stableyard.fi/v2/accounts/acct_123/display-profile \
  -u "$APP_ID:$APP_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "displayName": "Acme Downtown",
    "logoUrl": "https://cdn.example.com/acme-downtown.png",
    "expectedVersion": 0,
    "reason": "Set the payer-facing store identity"
  }'
```

| Field | Rule |
| - | - |
| `displayName` | Required, 1 to 120 characters, no control characters |
| `logoUrl` | Optional, a public HTTPS URL of up to 2048 characters with no embedded credentials, or null |
| `expectedVersion` | `0` the first time, then the current `displayProfile.version`. A stale value returns `409 conflict` |
| `reason` | Required, 10 to 500 characters. Each change is audited |

`displayProfile` can also be set on `POST /v2/accounts`.

## Errors

| Code | HTTP | When |
| - | - | - |
| `bad_request` | 400 | A wallet address that does not match its chain, two wallets marked `isPreferredSettlement`, a handle that is too short, too long or reserved, a namespace that is not your app's, or a `logoUrl` that is not public HTTPS |
| `not_found` | 404 | No such account in your app environment, or no such handle |
| `conflict` | 409 | `details.code: "payment_handle_immutable"` when the account already has a different handle; a handle another account holds; or a stale `expectedVersion` on acceptance or the display profile |

## Webhooks

Linking wallets, claiming a handle, and changing payment acceptance or the display profile each emit `account.updated`. The event says the account changed: read `GET /v2/accounts/{accountId}` for the current record. See [Webhooks](/webhooks/event-catalog#account-events).

## Related

<CardGroup cols={2}>
  <Card title="Universal Payment Account" icon="id-card" href="/universal-payment-account">
    The account wallets and handles attach to.
  </Card>

  <Card title="Settlement destinations" icon="location-dot" href="/settlement/destinations">
    Choose which linked wallet collected value lands in.
  </Card>

  <Card title="Stablecoin transfers" icon="coins" href="/payments/stablecoin-transfers">
    Send to a wallet, an account or a handle, across partners.
  </Card>

  <Card title="Sending payments" icon="paper-plane" href="/payments/sending-payments">
    Set a payment source and send from it.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.