Skip to main content

Sub-account settings API: recipes

The OpenAPI pages — Subaccounts API and the endpoint reference — document each operation in isolation. This cookbook chains the settings operations on /subaccounts/:id into the multi-step loops a reseller actually runs: read the effective sender, write a custom one, verify it, clear it, seed recipients, scope channels, and audit the inheritance chain. Everything here is a tenant-owned control: your parent organization owns the sub-accounts, the sender identities, and the allowlists; Orbit resolves and enforces them. Base URL is https://api.orbit.devotel.io/api/v1 throughout. Every response arrives in the shared envelope: a data object plus meta.request_id / meta.timestamp.
cURL
The settings routes are not covered by a typed SDK helper yet, so every example below is raw HTTP — fetch in TypeScript, requests in Python — and runs from any client. If you already use the Node or Python SDK, the same paths work through its escape hatch (client.request(...)), since these routes take no SDK-specific shaping.

0. Auth, roles, and the common path shape

Authenticate with a bearer token from an owner or admin of the parent organization. The settings plane (notification-email, recipients, allowlist, branding, pricing) is parent-admin territory: a token whose roles only cover a child organization gets 403 when it targets a sibling or its own parent. One exception mirrors the branding surface: a child org’s own admin/owner may read and write its own settings by passing its own organization id as :id. Unknown or invisible sub-account ids return 404, not 403 — the controller does not leak which child orgs exist to a caller without visibility. Treat a 404 on a id you just created as “wrong token”, not “wrong id”. These routes manage configuration, not message traffic, so there is no pagination to handle; each GET returns the full settings object for one sub-account.

1. Read the effective notification-email config

GET /subaccounts/:id/notification-email returns two layers: what this org configured, and what actually sends after inheritance resolution. The effective.source enum tells you where the resolver landed: subaccount (this org’s verified override), ancestor (the nearest parent with a verified override), or platform (the Orbit default sender).
cURL
A child that set and verified its own sender:
200
Read the two layers independently. configured (the top-level from_name / from_address / verified) is what a PUT wrote on this org; it may be all null on a fresh child. effective is what the notification emitter will actually use, resolved sub-account → parent → platform. When the effective sender comes from somewhere else, effective.source says where and effective.resolved_from_organization_id is the chain pointer — the organization id whose config won, or null when the org itself (or the platform default) won. A child inheriting from its parent therefore shows configured.from_address: null next to effective.source: "ancestor" and resolved_from_organization_id: "org_parent_1".
TypeScript
Python

2. Set a custom sender identity on a child

PUT /subaccounts/:id/notification-email writes the override. The write persists immediately, but the configured address only becomes the effective sender once its sending domain passes DKIM/SPF — the same verification evidence the email-senders surface tracks. Until then the resolver keeps using the ancestor or platform sender, so a successful PUT can still show effective.source: "ancestor". That is the designed activation gate, not a failed write.
cURL
200
Then poll until verified flips and the resolver promotes the override:
cURL
TypeScript
Python
A body that fails validation returns 422 with the offending field named:
422
Nothing persists on a 422, so fixing the body and retrying is always safe. The top validation entries are: from_address not a syntactically valid email, and from_name / from_address sent as non-string, non-null types. Send null, not "", when you mean “clear” (next recipe).

3. Clear the custom sender to restore the parent chain

PUT with from_address: null clears the whole override — the resolver then falls back to the nearest ancestor with a verified sender, or the platform default when no ancestor has one. Send from_name: null alongside; a name without an address is not a sender.
cURL
200
The response is computed after the write, so effective in the PUT response already shows the fallback the resolver will use from now on — no follow-up GET needed to confirm a clear.
TypeScript
Python

4. Per-subaccount notification recipients

By default a child’s operational notifications (rate-sheet digests, low-balance alerts) go to the inherited recipients. settings.notification_recipients overrides them per sub-account with two lists: rate_sheet_emails and balance_alert_emails.
cURL
200
PUT replaces the object. A non-empty list means “route these emails to exactly these addresses”; an empty or missing list means “inherit the previous hop’s recipients”, so you can override one category and leave the other inheriting:
cURL
The reader is deliberately tolerant: a stored value that is not the expected object (malformed JSONB from an old writer, a bare array, a string) collapses to null and the emitter falls back to inherited recipients instead of failing the send path. So a GET right after a legacy write can show notification_recipients: null — rewrite the full object to normalise it.
TypeScript
Python

5. Read and scope the channel-family allowlist

GET /subaccounts/:id/allowed-channels returns the channel families this sub-account may use. Absent any scoping, the child inherits every family the parent has enabled.
cURL
200
Scope a child to SMS + Email only (for example, a brand that has not finished WhatsApp sender approval):
cURL
Restore inherit-all with DELETE (or a PUT with an empty list — both clear the override):
cURL
The allowlist composes with the lifecycle endpoints: it narrows which families a healthy child can use, while suspend and reactivate gate the whole account. A suspended sub-account sends on no channel regardless of its allowlist; reactivating restores exactly the allowlist that was set, so scoping survives a suspend/reactivate cycle and you do not need to re-apply it.
TypeScript
Python

6. Chain-walk audit trail

When the panel badge says source: platform but ops expected an ancestor’s sender to win, walk the tree yourself: start at the child, print each hop’s configured state, and climb parent_organization_id until the chain resolves. The resolved_from_organization_id from recipe 1 is the fast answer; this walk is the audit trail that explains why — typically an ancestor’s override is present but unverified, so the resolver skipped it.
TypeScript
The common finding: an ancestor shows configured.from_address set but verified: false. The resolver only promotes verified senders, so the chain keeps climbing past it. Re-verify the ancestor’s domain (the email-senders surface) or clear the stale override with recipe 3.

7. White-label reseller checklist

One sequence an API-first reseller runs when a child org goes live: create, set the commercial knobs, brand it, claim and verify the custom domain, install the sender identity, seed recipients, scope channels, and smoke-test with one send.
cURL
Steps 5–7 are the settings surface this page covers; steps 1–4 link out to reseller subaccounts and the domain verification flow. Run step 8 with a child-scoped key so the send exercises the child’s allowlist and budget, not the parent’s.

Troubleshooting

  • 403 on every settings call. The token’s roles cover a child org, not the parent. Settings writes need owner or admin on the parent; a child admin can only target its own organization id.
  • 404 on a id that exists. The resource is hidden to this caller — the controller answers 404 instead of 403 for sub-accounts outside the token’s visibility. Re-issue the token from the parent org.
  • 422 with VALIDATION_ERROR. The top entries: from_address not a valid email; from_name/from_address sent as wrong types (send null to clear, not ""); an allowlist entry outside the known channel families; a recipients entry that is not an email address. Nothing persists on a 422.
  • The silent fallback. Your PUT returned 200 but effective.source is still platform or ancestor. The write succeeded — the activation gate did not pass. Check, in order: the new address’s domain is not DKIM/SPF-verified yet (verified: false), you cleared with "" instead of null on an earlier call, or you wrote a different :id than the one you are reading. Recipe 6 prints the whole chain and finds which hop is blocking.

See also