# Card Onramp for Your Own Wallets

For a tenant that already holds its users' wallets and wants only the card onramp: your server names each end user by your own id, gives the wallet the crypto goes to, and opens the purchase. Aurea runs the payment page, the KYC, the card payment and the delivery to that wallet.

## Overview

- **No Aurea account for your users.** You name each end user with your own id, `externalId`. The first call that names it makes it.
- **Your wallets, attested by you.** The signed call from your server is your statement that the wallet serves that user. The user signs nothing.
- **A default wallet per family** — `evm` (Ethereum, Base, Polygon, Avalanche, Celo) or `solana` — used when a purchase names no address. A purchase can also name its own wallet.
- **One call per purchase**, which answers a hosted link to send the user to (a page on Aurea's domain, in your name and colours) and a client secret if you embed the payment widget yourself.
- Apps where the user holds the key use the other flow: the user proves the address with a signature ([Card to Crypto](https://docs.aureahub.com/docs/guide-card-onramp.md)).

## Before You Start

- Ask your Aurea contact to switch on the card onramp and **tenant wallets** for your tenant, in the sandbox first. Until then the calls answer `403` `CARD_ONRAMP_TENANT_WALLETS_OFF` or `CARD_ONRAMP_OFF`.
- Give Aurea the addresses the hosted page may send users back to: only those are accepted as `returnUrl`.
- These routes are called from your server only, signed with your API key and secret as described in [Authentication](https://docs.aureahub.com/docs/authentication.md). They take no user token. Each signature is accepted once, so sign every call, retries included.

## The Calls

1. [Attest a default wallet](https://docs.aureahub.com/docs/card-wallets-wallet-put.md) — `PUT /v1/ramp/card/customers/{externalId}/wallets/{family}`, once and whenever it changes. Sending the same address again changes nothing.
2. [Open a purchase](https://docs.aureahub.com/docs/card-wallets-session-create.md) — `POST /v1/ramp/card/customers/{externalId}/sessions` with the pair, the amount and `customerIp`, the user's public IP address as your server saw it: the payment provider decides from it whether it can serve the user. Send the user to `hostedLink.url` (valid 30 minutes) and keep `session.id`.
3. Follow it with the [events to your server](https://docs.aureahub.com/docs/guide-events.md) (`ramp.card_session.updated`, which carries `customer.externalId`), or [read it](https://docs.aureahub.com/docs/card-wallets-session-get.md). When the hosted page is done it sends the user to your `returnUrl` with `from=onramp`, `onrampSession` and `status` added.

A session's `status` only moves forward: `initialized` → `requires_payment` → `fulfillment_processing` → `fulfillment_complete` (then `transactionId` is the on-chain hash), or `rejected`. The amounts are in `amounts.sourceCurrency`: the user can change the amount and the currency in the payment widget.

## Idempotency

Send an `Idempotency-Key` (for example your order id) when you open a purchase. The same key with the same request answers `200` with the same session and a fresh hosted link. The user's IP address is not part of the comparison, so a retry from another address still matches. The same key with another request answers `409` `IDEMPOTENCY_KEY_REUSED`.

## Errors

| Status | `details.code` | Meaning |
| --- | --- | --- |
| 401 | — | Signature missing, wrong, expired or used twice |
| 403 | `CARD_ONRAMP_TENANT_WALLETS_OFF` | Tenant wallets are not switched on in that environment |
| 403 | `CARD_ONRAMP_OFF` | The card onramp is not switched on in that environment |
| 400 | `CARD_ONRAMP_CUSTOMER_ID_INVALID` | `externalId` is not 1 to 100 letters, digits, `.` `_` `:` `@` `-` |
| 400 | `CARD_ONRAMP_CUSTOMER_IP_INVALID` | `customerIp` is not a public IPv4 or IPv6 address |
| 400 | `CARD_ONRAMP_ADDRESS_INVALID` | The address is not one of the family's, or has a wrong checksum |
| 400 | `CARD_ONRAMP_PAIR_UNAVAILABLE`, `CARD_ONRAMP_PAIR_NOT_SOLD_IN_CURRENCY`, `CARD_ONRAMP_AMOUNT_INVALID`, `RETURN_URL_NOT_ALLOWED` | The pair, its currency, the amount or the return URL |
| 404 | `CARD_ONRAMP_CUSTOMER_NOT_FOUND`, `CARD_ONRAMP_SESSION_NOT_FOUND`, `CARD_ONRAMP_WALLET_NOT_FOUND` | No such end user, purchase of that end user, or default wallet |
| 409 | `CARD_ONRAMP_WALLET_MISSING` | No address named and no default wallet of the pair's family |
| 403 | `CARD_ONRAMP_CUSTOMER_UNSUPPORTED` | The payment provider cannot serve this user |

Every refusal before the payment provider is called writes nothing: no end user, no wallet, no session.

## Checklist

- Tenant wallets switched on in the sandbox, return URLs registered.
- Calls signed on your server; the API secret never in an app or a browser.
- `customerIp` is the user's address, not your server's.
- A default wallet attested before a purchase that names no address.
- An `Idempotency-Key` per purchase; your webhook verifies each event's signature.
- A full purchase done in the sandbox before going live.

---

Web version: https://docs.aureahub.com/#guide-card-onramp-wallets
