Setting up a tenant from scratch
This guide covers provisioning a new tenant so it can operate credit lines: what is configured in Frontegg, what in Borrower Central, what in the CMS, and in which order.
The most common mistake is skipping the currency configuration. The CMS answers with default values instead of failing, so a half-configured tenant looks ready until the first credit-line write is rejected. Read The default trap before going live.
Where each piece lives
A tenant's configuration is spread across four systems. None of them is the single source of truth, and that is where most of the difficulty comes from.
- Frontegg — users, roles, permissions and tenant features. Without the right feature the application does not appear in the panel.
- Borrower Central — stores
main_partner_idin thecms-settingscollection, one row per tenant. It is the bridge to the CMS. - CMS (org-api) — clients and credit accounts. Validates every write against
dpa_settings. - dpa-api — owner of the
dpa_settingscollection, keyed bytenant. org-api only reads it, never writes it.
Frontegg prerequisites
Before touching Borrower Central, the tenant needs:
Active features. The credit screens sit behind feature flags. If they are missing, the user logs in but does not see the section:
| Feature | What it enables |
|---|---|
cms | Credit management, flows and debts |
altcard | Limits screen, recommendations and change sets |
Assigned roles. Provisioning requires workflows_admin. See the permissions table for the rest.
Features are enabled from the Frontegg console, not through the Borrower Central API. If the tenant lacks them, partner provisioning still works but the user will not see the screens — the endpoint tells you so in its response.
Step 1 — Create the partner and the DPA configuration
Requires the workflows_admin role.
| Field | Required | Notes |
|---|---|---|
name, shortName, email, taxId | yes | The partner's data in the CMS |
currency | yes | 3-letter ISO 4217 code. Normalised to uppercase; rejected with 400 if invalid |
timezone | no | E.g. America/Quito. If omitted, the CMS keeps its default |
currency is required on purpose. Leaving it optional meant leaving the default trap one forgotten field away.
What it does, exactly
The endpoint runs five steps in order. The first three are the main work; if any fails, nothing is persisted. The last two are checks that never fail the partner creation: if they fail, they are reported as warnings.
-
Checks the tenant has no partner yet. This is a "first partner" endpoint: if
cms-settings.main_partner_idalready has a value, it answers400withPARTNER_ALREADY_CONFIGURED. It does not overwrite. -
Creates the partner in the CMS (
POST /v2/partners). It distinguishes a CMS rejection from an outage:- a
4xxfrom the CMS is a data or permissions problem, returned as400withCMS_PARTNER_CREATE_REJECTEDincluding the CMS's real status and message; - a
5xxor a network failure returns503withCMS_PARTNER_CREATE_FAILED.
- a
-
Saves
main_partner_idin the tenant'scms-settings. From here on every CMS call uses that partner. -
Writes
dpa_settings(PATCH /v2/partners/{partnerId}/settings/dpa). This is the step that materialises the document in the CMS database, and it is whycurrencyis required in the body: without it the tenant ends up in the state described in The default trap. The ordering is forced — the endpoint is keyed by partner id, so it cannot run before the partner exists.If this write fails, the partner already exists and cannot be un-created, so the endpoint does not blow up: it answers
200withdpaSettingsConfigured: falseand theDPA_SETTINGS_WRITE_FAILEDwarning, which carries the real reason and how to retry. -
Queries the tenant's features in Frontegg. If
cmsoraltcardis missing, it reportsTENANT_MISSING_FEATURE.
Response
When something was left incomplete, warnings says so instead of hiding it:
A 200 with dpaSettingsConfigured: false is not a complete provisioning. The partner exists, but the tenant cannot write credit lines yet.
The default trap
If dpa_settings does not exist for the tenant, the system does not fail immediately: it fails later and somewhere else.
dpa-api synthesises a default configuration when it cannot find the document, and answers with it as if it existed. The invented values are:
| Field | Synthesised value |
|---|---|
defaults.currency | MXN |
taxRate | 16 (Mexican VAT) |
timezone | America/Mexico_City |
defaults.productId | 00000000-0000-0000-0000-000000000000 |
That produces a disconcerting sequence:
The read and the write consult the same thing through different paths: dpa-api with a fallback, org-api straight against the database. That is why the GET says everything is fine and the PATCH denies it.
Practical consequence: a tenant that operates in dollars and whose currency nobody configured ends up with credit accounts denominated in Mexican pesos, or with every write rejected. Always confirm with the checklist before declaring the tenant ready.
Step 2 — Create each borrower's CMS client
A Borrower Central borrower does not exist in the CMS until its client is created. This call creates it and stores the resulting id in borrowers.cms_client_ids.
A borrower without cms_client_ids is silently dropped from limit-change flows: the pipeline filters it out before processing and the batch can report as completed without having applied anything. Provision the client before creating recommendations.
The credit account is different from the client: it is not created here. It materialises with the first successful limit write. A GET on a non-existent account returns zeros without persisting anything, so seeing "current limit $0.00" does not mean the account exists.
Frontegg permissions table
Permissions Borrower Central checks, ordered by how many endpoints require them.
| Permission | Endpoints | What it enables |
|---|---|---|
bc.private.write | 136 | Change sets, credit limits, connectors, secrets |
borrowers.write | 135 | Create and edit borrowers, deals, assets |
borrowers.read | 112 | Reading the business core |
bc.private.read | 98 | Sensitive data, unmasking identities |
workflows.write | 70 | Edit workflows and tasks |
workflows.read | 47 | View workflows |
configurations.write | 45 | Data models, evaluation rules |
configurations.read | 31 | Read configuration |
workflows.execute | 27 | Run workflows |
bc.private.delete | 24 | Delete private entities |
executions.read | 16 | View executions |
bc.attachments.upload | 16 | Upload attachments |
executions.write | 15 | Modify executions |
configurations.delete | 12 | Delete configuration |
borrowers.delete | 10 | Delete borrowers |
bc.attachments.read | 7 | Download attachments |
executions.retry | 6 | Retry executions |
bc.attachments.delete | 6 | Delete attachments |
And the roles, checked directly on some endpoints:
| Role | Endpoints | Notes |
|---|---|---|
superadmin | 31 | Internal operations |
workflows_admin | 12 | Includes setup-first-partner and the CMS configuration |
maintainer | 3 | Maintenance |
The limits and change-set screens use bc.private.read and bc.private.write, not borrowers.*. A user with borrower permissions but without bc.private.write sees the screens and gets a rejection when trying to apply.
Verification
Before declaring the tenant ready, check all four layers.
1. The Frontegg features — that cms and altcard are active for the tenant.
2. main_partner_id in Borrower Central
It must return a mainPartnerId with a value.
3. dpa_settings actually exists. The settings GET is not enough: it answers with synthesised values. You have to look at the CMS database:
If it returns null, the tenant is not configured no matter what the GET says.
4. A test write. The only conclusive check:
A 200 confirms the whole chain works.
Common errors
| Code | Cause | Fix |
|---|---|---|
currency_settings_not_defined | No dpa_settings for the tenant, or its currency is empty | Create the configuration with currency (step 1). If setup answered dpaSettingsConfigured: false, retry the settings PATCH |
currency_not_supported | The currency sent does not match the tenant's | Align the currency, or migrate the existing accounts |
DPA_SETTINGS_WRITE_FAILED | The partner was created but its settings were not persisted | Warning, not an error. Retry PATCH /v2/partners/{id}/settings/dpa |
TENANT_MISSING_FEATURE | The tenant lacks cms or altcard in Frontegg | Warning, not an error. Enable it from the Frontegg console |
PARTNER_ALREADY_CONFIGURED | The tenant already has a main_partner_id | Use PUT /v1/application/cms-settings to change it |
CMS_PARTNER_CREATE_REJECTED | The CMS rejected the partner's data | Check cmsStatus and cmsMessage in the response |
CMS_PARTNER_CREATE_FAILED | The CMS is not responding | Retry; if it persists, it is an outage |
If a tenant already has credit accounts in the wrong currency, fixing dpa_settings does not migrate them. Existing accounts keep their currency and their writes will keep being rejected until they are migrated separately.