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

> ## Agent Instructions
> Money is always a decimal string at the currency's precision (for example "29.00"), paired with an ISO 4217 currency field. Never use minor units.
> Build against the sandbox first: base URL https://sandbox-api.bachs.io with sk_sandbox_ keys. Production is https://api.bachs.io with sk_live_ keys; going live is a key swap.
> Treat webhooks (for example collection.succeeded) as the source of truth for fulfilment, never client-side events or redirects.
> Subscriptions are created by completing a checkout for a recurring product. There is no direct create-subscription endpoint.
> IDs carry resource prefixes (cust_, prod_, sub_, chk_, inv_, ref_) and timestamps are ISO 8601 UTC.

# Give an account a dashboard

> Let the person behind an account see their money on connect.bachs.io or in the Bachs dashboard, and choose what they may change.

By default, nobody logs in to a connected account. You show the person behind it everything yourself, in your own app.

You can also let them log in. There are two ways:

* **A small dashboard at connect.bachs.io.** It carries your name, logo and colour. They never need a password.
* **The full Bachs dashboard at app.bachs.io.** They get their own login, the same one any Bachs merchant uses.

***

## Access modes

Each account has one access mode, in its `dashboard` field.

| `dashboard` | Who logs in | What they see |
| - | - | - |
| `none` | Nobody. This is the default. | Nothing. You show them what they need in your app. |
| `light` | The person behind the account, at connect.bachs.io. No password. | Their balance, payments, payouts, payout schedule, settings, and anything they still owe. |
| `full` | The person behind the account, at app.bachs.io, with a Bachs login. | The normal Bachs dashboard for this account: balance, payments, customers, payouts, settings, and anything they still owe. |

In every mode, you stay in charge. Nobody behind an account can take payments on their own. They cannot make payment links, checkouts, products, API keys or webhooks.

***

## Set the mode

Set `dashboard` when you create the account, or later on an update. Scope `connected_accounts:write`.

```bash theme={"dark"}
curl -X POST https://sandbox-api.bachs.io/v1/accounts \
  -H "Authorization: Bearer sk_sandbox_abc123xyz..." \
  -H "Content-Type: application/json" \
  -d '{
    "contact_email": "ada@example.com",
    "country": "NG",
    "entity_type": "individual",
    "configuration": {
      "recipient": { "capabilities": { "payouts": { "requested": true } } }
    },
    "dashboard": "light",
    "dashboard_features": { "payout_schedule": true }
  }'
```

To change an existing account, send the same fields to `POST /v1/accounts/{account_id}`.

The modes go up in this order: `none`, `light`, `full`. You can raise an account to any higher mode. You cannot lower it, because that takes access away from a person. If you need an account lowered, contact us.

You can also pick the mode in the dashboard, on the **Seller access** step when you create an account.

***

## Choose what they may change

`dashboard_features` decides what the person may change from their dashboard. Send only the switches you want to change. The rest keep their value.

| Switch | Starts | Lets the person |
| - | - | - |
| `view_payments` | On. You cannot turn it off or send it. | See their own payments |
| `payout_schedule` | Off | Change how often they are paid out |
| `manual_payouts` | Off | Withdraw by hand |
| `change_payout_destination` | Off | Change the bank account, mobile money or wallet they are paid to |
| `refunds` | Off | Refund their own payments |

These switches only limit the person. They never limit your API key.

When a switch is off and the person tries the action, they see: "Your platform manages this for you. Ask them if you need it changed."

An account in `none` mode reads every switch as `false`, including `view_payments`, because nobody logs in. If you raise it to `light` or `full`, the switches you set come back.

***

## Open the dashboard from your app

Put an **Open dashboard** button in your app. When the person taps it, make a login link and send them to it.

`POST /v1/accounts/{account_id}/login_links` · scope `connected_accounts:write`

```bash theme={"dark"}
curl -X POST https://sandbox-api.bachs.io/v1/accounts/acct_3Wq8ZfT1yHnJ5sVe/login_links \
  -H "Authorization: Bearer sk_sandbox_abc123xyz..."
```

```json theme={"dark"}
{
  "url": "https://connect.bachs.io/login/ll_9fK2mQ7xR4tV8wZ1",
  "expires_at": "2026-11-20T10:05:00Z"
}
```

The request has no body.

* The link works once, and lasts five minutes. Make it at the moment the person taps the button.
* Do not email it or save it. It will have expired by the time it is opened.
* When the person opens it, we email a six-digit code to the account's contact email. The code lasts ten minutes.
* A correct code opens their dashboard. They stay logged in for up to 12 hours.

The person can also log in at connect.bachs.io/login with that email. If the email is on accounts with several platforms, they pick which one to open.

> [!NOTE]
>
> Login links only work for accounts in `light` mode. Any other mode gets `400 login_links_need_light`. For `full`, see the next section.

***

## Give them the full dashboard

In `full` mode, the person behind the account signs in at app.bachs.io, like any Bachs merchant. The account shows up in their dashboard as an account of its own.

### The invite

We invite them by email. The invite goes to the account's contact email, or to their login email if they already logged in at connect.bachs.io. It names the account and your platform, so they know who it comes from. It lasts seven days.

* **When the account goes live**, we send the invite by ourselves. That is when its first capability turns on.
* **If the account is already live** when you raise it to `full`, we send it straight away.
* **To send it again**, for example after it expired or you fixed the contact email, call the invite endpoint. If you changed the contact email, the invite to the old address stops working.

`POST /v1/accounts/{account_id}/dashboard_invite` · scope `connected_accounts:write`

```bash theme={"dark"}
curl -X POST https://sandbox-api.bachs.io/v1/accounts/acct_3Wq8ZfT1yHnJ5sVe/dashboard_invite \
  -H "Authorization: Bearer sk_sandbox_abc123xyz..."
```

```json theme={"dark"}
{
  "dashboard": "full",
  "dashboard_access": "invited"
}
```

The request has no body.

> [!NOTE]
>
> We do not send invites in the sandbox. Test the rest of the flow there, and the invite in live mode.

### If they already use Bachs

A person who already has a Bachs login accepts the invite with that login. They do not need a second one. They then see two accounts in their dashboard, their own and this one, and switch between them.

The two accounts stay apart. Each has its own balance, payouts and checks. Their own account keeps working as before. The limits below apply only to the account you gave them.

### What they can and cannot do

They can see the account's balance, payments, customers and payouts, and answer anything it still owes.

They cannot take payments outside your platform on this account. That means no payment links, products, checkouts, API keys, webhooks or checkout settings. They also cannot see the Connect pages. If they try, they get `403 CONNECTED_ACCOUNT_NOT_ALLOWED`.

The `dashboard_features` switches below work in `full` mode too. When a switch is off and they try the action, they see: "Your platform manages this for your account."

We never invite an email that belongs to someone on your own team. That gets `403 login_email_is_platform_member`.

***

## Contact email and login email

An account has two emails, so that you can never log in as the person behind it.

* **Contact email.** Yours to set and change, as today. We send the first login code here.
* **Login email.** The email the person used to log in for the first time. It is set when they enter their first correct code.

Until the person logs in, the contact email is the only one. Change it freely, and the next code goes to the new address.

After they log in:

* changing the contact email does not move their login;
* only the person, from their own settings, or our team can change their login email.

We never send a code to an email that belongs to someone on your own team. That gets `403 login_email_is_platform_member`.

***

## Read the mode

`GET /v1/accounts/{account_id}` returns three fields.

| Field | Means |
| - | - |
| `dashboard` | `none`, `light` or `full` |
| `dashboard_features` | All five switches, each `true` or `false` |
| `dashboard_access` | `not_invited`: nobody has logged in yet. `invited`: a `full` invite is out, and nobody has accepted it yet. `active`: the person has logged in at least once, or accepted the `full` invite. |

```json theme={"dark"}
{
  "id": "acct_3Wq8ZfT1yHnJ5sVe",
  "dashboard": "light",
  "dashboard_features": {
    "view_payments": true,
    "payout_schedule": true,
    "manual_payouts": false,
    "change_payout_destination": false,
    "refunds": false
  },
  "dashboard_access": "active"
}
```

`account.updated` carries `dashboard` and `dashboard_access` too.

***

## Errors

| HTTP | `error_code` | When |
| - | - | - |
| 400 | `invalid_dashboard_mode` | `dashboard` is not a mode you can use. Send `none`, `light` or `full`. |
| 400 | `unknown_dashboard_feature` | A switch name in `dashboard_features` does not exist, or you sent `view_payments`. |
| 400 | `invalid_dashboard_feature_value` | `dashboard_features` is not an object, or a value is not `true` or `false`. |
| 400 | `dashboard_cannot_be_lowered` | You tried to lower the mode. Contact us. |
| 400 | `dashboard_platform_only` | The account itself tried to change its access. Only you, the platform, can. |
| 400 | `login_links_need_light` | You asked for a login link for an account that is not in `light` mode. |
| 400 | `dashboard_invite_needs_full` | You asked for an invite for an account that is not in `full` mode. |
| 400 | `dashboard_invite_needs_email` | The account has no contact email to send the invite to. Set one first. |
| 400 | `dashboard_access_already_active` | The person already accepted the invite. There is nothing to send. |
| 403 | `login_email_is_platform_member` | The email belongs to someone on your own team. |
| 403 | `CONNECTED_ACCOUNT_NOT_ALLOWED` | A person in `full` mode tried something only you can do, like making a payment link. |
| 409 | `dashboard_invite_conflict` | Someone on the account's team already invited this email, or it is already on the team. |

See [Error Reference](https://docs.bachs.io/api-reference/error-reference.md) for the error envelope.

***

## Related

* [Accounts](https://docs.bachs.io/connect/accounts.md)
* [Onboarding](https://docs.bachs.io/connect/onboarding.md)
* [Payouts](https://docs.bachs.io/connect/payouts.md)
* [account.updated](https://docs.bachs.io/guides/webhooks/events/account-updated.md)


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