# Create Onboarding Session

Get a hosted onboarding (KYC) link for the user and send them there.

## Overview

The bank ramp runs the whole KYC flow — identity verification, documents, questionnaires — on its hosted page. Call this endpoint, open the returned `onboardingUrl` in a browser or an in-app browser, and the bank ramp sends the user back to your `returnUrl` when they finish.

- `returnUrl` is handed to the bank ramp unchanged, and the bank ramp sends the user back to an `https://` address only: any other answers `400` with `details.code` `RETURN_URL_NOT_HTTPS`, before the bank ramp is called. Use an `https://` page, not an app deep link. (Payouts are different: there Aurea proxies the return, so deep links work.)
- **The return URL must be one your tenant allows.** Once the Aurea operator has added your tenant's return URLs ([Bank Ramp Setup](https://docs.aureahub.com/docs/bank-setup.md)), any other `returnUrl` answers `400` with `details.code` `RETURN_URL_NOT_ALLOWED`, and the bank ramp is not called. A tenant with no return URLs yet is not checked.
- **Retries.** Send an `Idempotency-Key` header to make a retry safe: the same key with the same body answers with the first answer, and the response header `idempotency-replayed` is `true`. See [Idempotency](https://docs.aureahub.com/docs/idempotency.md).
- **KYC must be switched on** for your tenant in the environment `isTestnet` names ([Tenant Settings](https://docs.aureahub.com/docs/bank-settings.md)), otherwise `403` with `details.code` `NOAH_FUNCTION_OFF`, before the bank ramp is called and before an `Idempotency-Key` is replayed.
- **One bank ramp customer per user and environment, for good.** The bank ramp rejects a second KYC application for a person, so Aurea always asks the bank ramp for the same customer: calling again resumes the user's onboarding, it never starts another one.
- `customerType` (`Individual`, the bank ramp's default, or `Business`) and `locale` (the language of the bank ramp's page: `en`, `es`, `fr`, `de`, `it`, `pt`) are sent to the bank ramp only when you give them. Once the bank ramp has a type for the user, asking for the other one answers `409` with `details.code` `NOAH_CUSTOMER_TYPE_MISMATCH` and `details.customerType`, without calling the bank ramp.
- `isTestnet: true` uses the bank ramp's sandbox; the default is production (see [Bank Ramp Setup](https://docs.aureahub.com/docs/bank-setup.md)). Each environment keeps its own profile for the user, with its own KYC: a user approved in sandbox still onboards separately in production. Use the same value later for the user's status, deposits and payouts.

When the user comes back, call [Sync KYC Status](https://docs.aureahub.com/docs/payin-sync-status.md) to read the result from the bank ramp without waiting for the webhook.

## When the Bank Ramp Says No

the bank ramp's hosted onboarding ends with an **available accounts** step, after the country, the email and the agreements. When the bank ramp has no virtual account to offer that person, that step is where the flow stops: *"based on the information you provided, we are currently unable to offer any virtual accounts to you due to regulatory restrictions"*. The page has no way forward — only Help and Exit.

**Sending no `fiatOptions` does not skip that step.** Measured against the bank ramp's sandbox with fresh customers: a session that applied for EUR and a session that applied for nothing at all reached the same screen, in two different countries. Treat `fiatOptions` as what Aurea forwards to the hosted flow, not as a way to steer the hosted flow.

**Neither Aurea nor you can see that this happened.** The bank ramp does not create the customer at all, so there is no status anywhere to read: [Onboarding Status](https://docs.aureahub.com/docs/payin-status.md) goes on answering `not_started`, [Sync KYC Status](https://docs.aureahub.com/docs/payin-sync-status.md) goes on answering that the bank ramp does not have the customer, and nothing tells this user apart from one who never opened the link. This is a limit of what the bank ramp exposes, not an omission in Aurea.

So **treat a status that has not moved after the user came back as a failure, not as a reason to send them again** — a second link leads to the same wall. Count the attempts on your side: after the second one, tell the user the verification did not go through and give them a way to reach a person, instead of the same button.

**A payout needs no virtual account** — a payout to a proven address settles on-chain — but that does not currently carry a user past this step, because the step runs either way. Send `fiatOptions: []` when your user genuinely wants no account, so the bank ramp is not asked for one; do not send it expecting a different hosted outcome.

## Outcome

For a user Aurea already has, it first reads the customer from the bank ramp (and stores it, as [Sync KYC Status](https://docs.aureahub.com/docs/payin-sync-status.md) does). `outcome` then says what happened; only `session` has a link.

| outcome | What to do |
| --- | --- |
| session | Open `onboardingUrl` before `expiresAt`: the bank ramp's link lasts **1 hour**, **24 hours** for a Business customer. A link Aurea already gave with more than five minutes left is given again **to the same request** — the same `returnUrl`, `fiatOptions`, `customerType`, `locale` and `metadata`; a request that changes any of them gets a new session from the bank ramp, made for what it asked. After those five minutes, call this endpoint again for a new one. A user the bank ramp approved but asks something of (`verification.nextStep` `continue_onboarding`) gets a session too: The bank ramp shows what is missing. |
| approved | the bank ramp approved the user and asks for nothing: `alreadyCompleted` is `true`. Continue with [Initiate Deposit](https://docs.aureahub.com/docs/payin-initiate.md). An approved user stays approved when the bank ramp's answer says nothing about the verification; only the bank ramp declining them changes it. |
| in_review | the bank ramp is reviewing the user and needs nothing now. Show that the verification is in progress; the bank ramp's `Customer` webhook, or [Sync KYC Status](https://docs.aureahub.com/docs/payin-sync-status.md), gives the result. |
| declined | the bank ramp declined the user for good: no session is created and the bank ramp is not asked. |

`verification` is the user's verification after the call, as [Onboarding Status](https://docs.aureahub.com/docs/payin-status.md) returns it. When Aurea had no profile for the user in that environment and the bank ramp already has the customer, the profile is created and its `customerId` returned.

## Endpoint

### `POST /v1/ramp/bank/payin/onboard-session`

Authentication: bearer token required.

Creates, or reuses, a hosted onboarding session for the authenticated user.

**Headers**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | no | Optional. Up to 255 letters, digits, - or _. The same key with the same body answers with the first answer instead of doing it again; see [Idempotency](https://docs.aureahub.com/docs/idempotency.md) |

**Request body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `returnUrl` | string | yes | https:// URL, 10 to 1000 characters, the bank ramp redirects the user to when onboarding ends; any other answers 400 RETURN_URL_NOT_HTTPS |
| `fiatOptions` | object[] | no | Virtual accounts to apply for on the user's behalf. Each item: { fiatCurrency: EUR \| USD \| GBP }. Omitted means EUR only; **an empty list applies for none**, so the bank ramp receives no FiatOptions — it does not change what the bank ramp’s hosted page shows: see [When the Bank Ramp Says No](https://docs.aureahub.com/docs/payin-session.md) |
| `metadata` | object | no | Up to 10 text values attached to the bank ramp customer; any other value answers 400 |
| `isTestnet` | boolean | no | true = the bank ramp's sandbox, false = production (default false) |
| `customerType` | string | no | Individual (the bank ramp's default) or Business. Another type than the one the bank ramp has answers 409 NOAH_CUSTOMER_TYPE_MISMATCH |
| `locale` | string | no | Language of the bank ramp's page: en, es, fr, de, it or pt (the bank ramp's default en) |

**Responses**

`201` Session

```json
{
  "customerId": "3f6c2a8e-5b1d-4e7a-9c3f-2d8e1b4a6c90",
  "noahCustomerId": "aurea_5d1f0c9b2e7a4c3d8f6b1a0e9c2d7b4f5a3e",
  "onboardingUrl": "<hosted onboarding URL>",
  "expiresAt": "2026-09-12T09:30:00.000Z",
  "alreadyCompleted": false,
  "outcome": "session",
  "message": "Onboarding session created. Direct customer to the onboardingUrl to complete KYC verification.",
  "verification": {
    "status": null,
    "customerType": null,
    "regions": [],
    "actionsRequired": [],
    "agreements": [],
    "nextStep": "start_onboarding"
  }
}
```

`201` In review

```json
{
  "customerId": "3f6c2a8e-5b1d-4e7a-9c3f-2d8e1b4a6c90",
  "noahCustomerId": "aurea_5d1f0c9b2e7a4c3d8f6b1a0e9c2d7b4f5a3e",
  "onboardingUrl": "",
  "alreadyCompleted": false,
  "outcome": "in_review",
  "message": "The bank ramp is reviewing this customer: no onboarding session is needed now. The bank ramp's Customer webhook reports the result.",
  "verification": {
    "status": "Pending",
    "customerType": "Individual",
    "regions": [
      { "entity": "Lt", "region": "EU", "status": "Pending", "rejection": null }
    ],
    "actionsRequired": [],
    "agreements": [],
    "nextStep": "wait_for_review"
  }
}
```

`400` Return URL not allowed

```json
{
  "statusCode": 400,
  "error": "BadRequestError",
  "message": "This return URL is not one the tenant allows. Ask the Aurea operator to add it.",
  "details": { "code": "RETURN_URL_NOT_ALLOWED" }
}
```

`409` Another customer type

```json
{
  "statusCode": 409,
  "error": "ConflictError",
  "message": "The bank ramp has this customer as Individual: an onboarding session for another customer type is not possible.",
  "details": { "code": "NOAH_CUSTOMER_TYPE_MISMATCH", "customerType": "Individual" }
}
```

`403` KYC switched off

```json
{
  "statusCode": 403,
  "error": "ForbiddenError",
  "message": "The bank ramp's KYC is not switched on for this tenant in sandbox.",
  "details": { "code": "NOAH_FUNCTION_OFF", "function": "kyc", "environment": "sandbox" }
}
```

`503` Bank Ramp not configured

```json
{
  "statusCode": 503,
  "error": "NoahApiError",
  "message": "The bank ramp is not configured for this tenant in sandbox",
  "details": { "code": "NOAH_NOT_CONFIGURED" }
}
```

When the user is already approved, the `201` body has `"outcome": "approved"`, `"alreadyCompleted": true`, `"onboardingUrl": ""` and the message `Customer already completed onboarding. Use /initiate to get virtual IBAN.` When the bank ramp declined the user for good, `"outcome": "declined"` and the message `Noah declined this customer for good: no onboarding session is created.`

## Implementation

```javascript
// 1. Start (or resume) the onboarding
async function startBankOnboarding(token, { sandbox = false } = {}) {
  const res = await fetch('https://api.aureahub.com/v1/ramp/bank/payin/onboard-session', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
    body: JSON.stringify({
      returnUrl: 'https://app.example.com/kyc/done',
      fiatOptions: [{ fiatCurrency: 'EUR' }],
      isTestnet: sandbox
    })
  });
  if (!res.ok) throw new Error(`Onboarding session failed: ${res.status}`);

  const { outcome, onboardingUrl } = await res.json();
  switch (outcome) {
    case 'session':
      window.location.href = onboardingUrl; // or open it in an in-app browser
      return;
    case 'approved':
      return showDepositOptions();
    case 'in_review':
      return showVerificationInProgress(); // the bank ramp's Customer webhook brings the result
    case 'declined':
      return showKycDeclined();
  }
}

// 2. On https://app.example.com/kyc/done — refresh the status from the bank ramp
async function onKycReturn(token, { sandbox = false } = {}) {
  const res = await fetch('https://api.aureahub.com/v1/ramp/bank/payin/sync-status', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
    body: JSON.stringify({ isTestnet: sandbox }) // the same value as the session
  });
  const { canInitiateDeposit, kycStatus, onboardingStatus } = await res.json();

  if (canInitiateDeposit) return showDepositOptions();
  if (kycStatus === 'rejected') return showKycRejected();

  // Nothing moved: The bank ramp has no customer, so its page produced nothing — the user
  // was turned away on it. A second link leads to the same wall, so count and stop.
  if (onboardingStatus === 'not_started' && kycStatus === 'not_started') {
    const attempts = countKycAttempt(userId); // your own storage, not Aurea's
    return attempts >= 2 ? showKycUnavailable() : offerKycOnceMore();
  }

  showKycInProgress(); // the bank ramp is reviewing — its Customer webhook brings the result
}
```

---

Web version: https://docs.aureahub.com/#payin-session
