AltScore
Loan and credit line management

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_id in the cms-settings collection, 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_settings collection, keyed by tenant. 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:

FeatureWhat it enables
cmsCredit management, flows and debts
altcardLimits 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

POST /v1/application/cms-settings/setup-first-partner

Requires the workflows_admin role.

{
  "name": "Automotores Continental S.A.",
  "shortName": "Automotores",
  "email": "operaciones@automotores.example",
  "taxId": "1791234567001",
  "currency": "USD",
  "timezone": "America/Quito"
}
FieldRequiredNotes
name, shortName, email, taxIdyesThe partner's data in the CMS
currencyyes3-letter ISO 4217 code. Normalised to uppercase; rejected with 400 if invalid
timezonenoE.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.

  1. Checks the tenant has no partner yet. This is a "first partner" endpoint: if cms-settings.main_partner_id already has a value, it answers 400 with PARTNER_ALREADY_CONFIGURED. It does not overwrite.

  2. Creates the partner in the CMS (POST /v2/partners). It distinguishes a CMS rejection from an outage:

    • a 4xx from the CMS is a data or permissions problem, returned as 400 with CMS_PARTNER_CREATE_REJECTED including the CMS's real status and message;
    • a 5xx or a network failure returns 503 with CMS_PARTNER_CREATE_FAILED.
  3. Saves main_partner_id in the tenant's cms-settings. From here on every CMS call uses that partner.

  4. Writes dpa_settings (PATCH /v2/partners/{partnerId}/settings/dpa). This is the step that materialises the document in the CMS database, and it is why currency is 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 200 with dpaSettingsConfigured: false and the DPA_SETTINGS_WRITE_FAILED warning, which carries the real reason and how to retry.

  5. Queries the tenant's features in Frontegg. If cms or altcard is missing, it reports TENANT_MISSING_FEATURE.

Response

{
  "partnerId": "bb2e7612-5298-417e-8b9b-df61c48c3403",
  "mainPartnerIdSet": true,
  "dpaSettingsConfigured": true,
  "warnings": []
}

When something was left incomplete, warnings says so instead of hiding it:

{
  "partnerId": "bb2e7612-5298-417e-8b9b-df61c48c3403",
  "mainPartnerIdSet": true,
  "dpaSettingsConfigured": false,
  "warnings": [
    {
      "code": "DPA_SETTINGS_WRITE_FAILED",
      "message": "The partner was created but its DPA settings could not be saved: CMS down. Until they exist the CMS answers reads with a synthesised MXN default and rejects every credit-account write with currency_settings_not_defined. Retry with PATCH /v2/partners/bb2e7612-.../settings/dpa."
    },
    {
      "code": "TENANT_MISSING_FEATURE",
      "feature": "altcard",
      "message": "The tenant does not have the 'altcard' feature enabled in Frontegg, so the related screens will not be visible. Enable it from the Frontegg console."
    }
  ]
}

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:

FieldSynthesised value
defaults.currencyMXN
taxRate16 (Mexican VAT)
timezoneAmerica/Mexico_City
defaults.productId00000000-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

POST /v1/borrowers/{borrowerId}/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.

PermissionEndpointsWhat it enables
bc.private.write136Change sets, credit limits, connectors, secrets
borrowers.write135Create and edit borrowers, deals, assets
borrowers.read112Reading the business core
bc.private.read98Sensitive data, unmasking identities
workflows.write70Edit workflows and tasks
workflows.read47View workflows
configurations.write45Data models, evaluation rules
configurations.read31Read configuration
workflows.execute27Run workflows
bc.private.delete24Delete private entities
executions.read16View executions
bc.attachments.upload16Upload attachments
executions.write15Modify executions
configurations.delete12Delete configuration
borrowers.delete10Delete borrowers
bc.attachments.read7Download attachments
executions.retry6Retry executions
bc.attachments.delete6Delete attachments

And the roles, checked directly on some endpoints:

RoleEndpointsNotes
superadmin31Internal operations
workflows_admin12Includes setup-first-partner and the CMS configuration
maintainer3Maintenance

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

GET /v1/application/cms-settings

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:

db.dpa_settings.findOne({ tenant: "<tenant>" })

If it returns null, the tenant is not configured no matter what the GET says.

4. A test write. The only conclusive check:

PATCH /v2/clients/{clientId}/credit-accounts/dpa
{ "assigned": { "amount": "1.00", "currency": "USD" }, "reason": "configuration check" }

A 200 confirms the whole chain works.

Common errors

CodeCauseFix
currency_settings_not_definedNo dpa_settings for the tenant, or its currency is emptyCreate the configuration with currency (step 1). If setup answered dpaSettingsConfigured: false, retry the settings PATCH
currency_not_supportedThe currency sent does not match the tenant'sAlign the currency, or migrate the existing accounts
DPA_SETTINGS_WRITE_FAILEDThe partner was created but its settings were not persistedWarning, not an error. Retry PATCH /v2/partners/{id}/settings/dpa
TENANT_MISSING_FEATUREThe tenant lacks cms or altcard in FronteggWarning, not an error. Enable it from the Frontegg console
PARTNER_ALREADY_CONFIGUREDThe tenant already has a main_partner_idUse PUT /v1/application/cms-settings to change it
CMS_PARTNER_CREATE_REJECTEDThe CMS rejected the partner's dataCheck cmsStatus and cmsMessage in the response
CMS_PARTNER_CREATE_FAILEDThe CMS is not respondingRetry; 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.

On this page