Connector Accounts
Gatelithix v2.3 introduced the connector account model. A connector account is a per-merchant, per-mode binding between one of your merchants and a specific set of processor credentials (a Stripe API key, an NMI security key, a FluidPay API key, a TSYS device credential, …). Every charge the gateway settles on your behalf is settled through exactly one connector account — there is no platform-default fallback.
Breaking change in v2.3. Platform-default connector credential environment variables are removed. Every merchant must have at least one connector account for each connector + mode they process through. See the Migration Guide for the env-var → connector-account mapping.
What a connector account is
A connector account is a row in the connector_accounts table that binds a
merchant to a specific (connector, mode, key_kind) triple via a Secret
Manager path. Each row carries:
| Field | Purpose |
|---|---|
short_id | Public-facing identifier (ca_abc123def4). Use this in API request bodies and headers. |
id | Internal UUID. Not shown in dashboards; used for internal joins only. |
connector_id | Which processor this binds to — stripe, nmi, fluidpay, tsys. |
mode | Environment: live or test. |
key_kind | Which credential family — e.g. api_key, security_key, device_credential. |
label | Free-form human tag. Unique per (merchant, connector, mode). Examples: us_usd, eu_eur, staging. |
is_default | Exactly one account per (merchant, connector, mode, key_kind) may be marked default. |
credential_ref | Google Secret Manager resource path. Never exposes the secret itself. |
is_active | Soft-delete flag. Deactivated accounts cannot settle new charges. |
The short_id is what you use everywhere in the public API. The internal
UUID never appears in request bodies, request headers, or response
connector_account_short_id fields.
Why multi-account
One merchant legitimately needs more than one connector account in several
real-world cases. Two of the most common (see SCENARIOS.md §1 for the full
matrix):
-
Geographic/currency split on the same connector. A merchant has a US-USD Stripe account for domestic customers and a separate EU-EUR Stripe account for European customers. Both are
connector_id=stripe,mode=live, but differentlabels (us_usd,eu_eur) and different Stripe credentials. The caller picks which account settles each charge via theconnector_accountbody field. -
Separate test sandbox per region. The same merchant has an
eu_eur_stagingStripe sandbox alongsideus_usd_staging, so QA in each region runs against its own processor sandbox. All three areconnector_id=stripe, butmode=testwith distinct labels.
Other common patterns: one connector account per business entity under a parent merchant, an overflow account kept cold for failover, a dedicated subscription-only account isolated from one-shot sales.
is_default semantics and override rules
Default resolution
For every (merchant, connector, mode, key_kind) there is at most one
account with is_default=true. When a payment request arrives without an
explicit connector_account, the gateway:
- Picks the connector (routing logic — see Payment Routing).
- Looks up the merchant’s default account for that
(connector, mode, key_kind)triple. - Settles the charge through that account.
If no default exists (for example, because the merchant has multiple active
accounts and hasn’t marked one as the default), the gateway returns
400 connector_account_required with a list of available accounts the
caller can choose from.
Override precedence — body wins
Callers can override the default per-request via either:
- Body field:
"connector_account": "ca_abc123def4"in the JSON request body. - Header:
X-Connector-Account: ca_abc123def4.
The body field wins over the header if both are set and agree. If both are
set and disagree, the gateway returns 400 connector_account_ambiguous.
The body-wins rule keeps precedence predictable across SDKs that synthesise
headers differently.
Capture / refund / void cannot override
Once a PaymentIntent has been authorized through account ca_abc123def4,
every follow-on operation (capture, refund, void) MUST settle through the
same account. Passing a different connector_account on capture/refund/
void returns 400 connector_account_override_forbidden. This is a hard
PCI-audit invariant — an authorized charge cannot be settled by a
different credential.
Why this matters for PCI scope. Cross-account capture would let a compromised credential settle a transaction it never authorized, which violates the chain-of-custody PCI DSS expects from a Level 1 Service Provider. We enforce this at the handler level regardless of what the dashboard, SDK, or direct API caller sends.
Create, rotate, delete
Connector accounts are managed through the admin API under
/admin/merchants/{merchantId}/connector-accounts. See the
API Reference for the full
OpenAPI spec.
Create
POST /admin/merchants/{merchantId}/connector-accounts
Authorization: Bearer sk_live_your_admin_key
Idempotency-Key: ca_create_01HK...
Content-Type: application/json
{
"connector_id": "stripe",
"mode": "live",
"key_kind": "api_key",
"label": "us_usd",
"is_default": true,
"credential_ref": "projects/gatelithix-core/secrets/merchant-mrc_xyz789abc0-stripe-live"
}Response — 201 Created:
{
"id": "aa0e8400-e29b-41d4-a716-446655440aaa",
"short_id": "ca_abc123def4",
"merchant_id": "22b1e200-c29b-41d4-a716-446655440000",
"merchant_short_id": "mrc_xyz789abc0",
"connector_id": "stripe",
"mode": "live",
"key_kind": "api_key",
"label": "us_usd",
"is_default": true,
"credential_ref": "projects/gatelithix-core/secrets/merchant-mrc_xyz789abc0-stripe-live",
"is_active": true,
"created_at": "2026-04-24T12:00:00Z",
"updated_at": "2026-04-24T12:00:00Z"
}Rotate credentials
Rotate by updating the Secret Manager secret version — the connector
account row is unchanged; the credential_ref resolves to the latest
version at charge time. If you need to move the account to a new Secret
Manager path entirely, PATCH the account with a new credential_ref:
PATCH /admin/merchants/{merchantId}/connector-accounts/{caid}
Authorization: Bearer sk_live_your_admin_key
Idempotency-Key: ca_rotate_01HK...
Content-Type: application/json
{
"credential_ref": "projects/gatelithix-core/secrets/merchant-mrc_xyz789abc0-stripe-live-v2"
}Delete
Connector accounts are soft-deleted — DELETE sets is_active=false.
Deactivated accounts:
- Cannot settle new charges (return
400 connector_account_inactive). - Remain joinable for historical PaymentIntent records (capture/refund/
void on older intents MAY fail with
409 originating_account_unavailableif the account was deleted between authorize and settlement). - Cannot be reactivated by
PATCH is_active=truein v2.3 — create a new account instead.
DELETE /admin/merchants/{merchantId}/connector-accounts/{caid}
Authorization: Bearer sk_live_your_admin_key
Idempotency-Key: ca_delete_01HK...Response — 204 No Content.
Troubleshooting
All connector-account errors surface through the standard error envelope (see Error Codes). The codes introduced in v2.3:
| Error code | HTTP | Likely cause | Fix |
|---|---|---|---|
connector_account_required | 400 | Merchant has 0 active accounts, OR has ≥2 active accounts but none marked default, and caller didn’t specify one. | Create a default account, OR pass connector_account in the body / X-Connector-Account header. Response includes available_accounts for authenticated callers. |
connector_account_not_found | 404 | connector_account short_id does not exist, OR exists but is owned by a different merchant. | Verify the short_id matches an account you own (dashboard → merchant → Connector Accounts). The 404 is deliberate anti-enumeration — we never leak that an account exists under another merchant. |
connector_account_inactive | 409 | Account exists and is owned by the caller’s merchant, but is_active=false. | Create a new active account; inactive accounts cannot be reactivated. |
connector_account_mismatch | 409 | Override connector_account resolves to an account on a different connector than the request routed to. | Use a connector account whose connector_id matches the routed connector. |
connector_account_mode_mismatch | 409 | Override connector_account resolves to an account on a different mode (live vs test). | Use a connector account whose mode matches the request mode. |
invalid_connector_account | 400 | Body field or header value is neither a valid short_id nor a UUID. | Send a cac_... short_id or a canonical UUID. |
connector_account_ambiguous | 400 | Both body connector_account and X-Connector-Account header are set and disagree. | Pick one — body wins when both are set and agree. |
connector_account_override_forbidden | 400 | Capture/refund/void specified a different connector_account than the originating authorize/sale used. | Drop the override; follow-on ops inherit the authorize’s account automatically. |
Tracked for v2.4:
connector_account_default_conflict,connector_account_label_conflict, andoriginating_account_unavailablehave store-level enforcement in v2.3 but do not yet have dedicatederror.codevalues — they surface as generic 409 errors today.
See the Migration Guide for env-var → connector-account mappings during upgrade.
Next steps
- Migration Guide — v2.3 — step-by-step upgrade for callers on v2.2.x.
- Authentication — Selecting a connector account — request-level override mechanics.
- Payment Routing — how routing picks the connector, and how
connector_accountpicks which account of that connector. - Error Codes — full error-code reference.