Skip to Content
Connector Accounts

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:

FieldPurpose
short_idPublic-facing identifier (ca_abc123def4). Use this in API request bodies and headers.
idInternal UUID. Not shown in dashboards; used for internal joins only.
connector_idWhich processor this binds to — stripe, nmi, fluidpay, tsys.
modeEnvironment: live or test.
key_kindWhich credential family — e.g. api_key, security_key, device_credential.
labelFree-form human tag. Unique per (merchant, connector, mode). Examples: us_usd, eu_eur, staging.
is_defaultExactly one account per (merchant, connector, mode, key_kind) may be marked default.
credential_refGoogle Secret Manager resource path. Never exposes the secret itself.
is_activeSoft-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):

  1. 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 different labels (us_usd, eu_eur) and different Stripe credentials. The caller picks which account settles each charge via the connector_account body field.

  2. Separate test sandbox per region. The same merchant has an eu_eur_staging Stripe sandbox alongside us_usd_staging, so QA in each region runs against its own processor sandbox. All three are connector_id=stripe, but mode=test with 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:

  1. Picks the connector (routing logic — see Payment Routing).
  2. Looks up the merchant’s default account for that (connector, mode, key_kind) triple.
  3. 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-deletedDELETE 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_unavailable if the account was deleted between authorize and settlement).
  • Cannot be reactivated by PATCH is_active=true in 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 codeHTTPLikely causeFix
connector_account_required400Merchant 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_found404connector_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_inactive409Account 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_mismatch409Override 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_mismatch409Override 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_account400Body field or header value is neither a valid short_id nor a UUID.Send a cac_... short_id or a canonical UUID.
connector_account_ambiguous400Both 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_forbidden400Capture/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, and originating_account_unavailable have store-level enforcement in v2.3 but do not yet have dedicated error.code values — they surface as generic 409 errors today.

See the Migration Guide for env-var → connector-account mappings during upgrade.


Next steps