Manager accounts
A manager account lets one organization operate many client (“managed”) organizations with a single credential. It’s the pattern a portal uses to run an Erdo org per customer: instead of minting and pasting a separate API key for every tenant, you provision each client org under your manager org and operate them all with one manager key. This mirrors how a Google Ads MCC (manager account) operates many ad accounts, and how a SaaS control plane runs a tenant per customer. If you’re building a product on top of Erdo that gives each of your customers their own isolated org, this is the surface you want.How it works
Your active org is the manager — you must be an admin or owner of it. Two things hang off it:- Managed orgs — the client tenants you provision. Each is a full, isolated Erdo organization: its own datasets, pages, knowledge, and RBAC. Creating one records a management link (kept as an audit trail even after you stop managing) and its own identity inside the org, so downstream setup seeds with a real owner.
- The manager key — one non-expiring API key, held by your manager org, that is a member of
every org you manage. You operate a specific managed org by naming it in the request: pass its
slug or id as the
X-Organization-IDheader (orerdo --org <slug>on the CLI). The backend re-validates the manager’s membership in that org on every request, so revoking management ends access immediately.
Provisioning a client org from a portal
The typical portal flow, once per customer:1
Create the client org
Call
create managed organization with the customer’s name. You get back the new org’s slug —
the durable handle you store against that customer.2
Mint the manager key once
Call
create manager key to mint the single credential your portal holds. It’s returned exactly
once; store it as a secret. You only do this once (or again to rotate).3
Operate the org
Send the manager key with
X-Organization-ID set to the customer’s org slug to do anything inside
that org — create datasets, build pages, run agents. The same key operates every customer.4
Off-board
Call
revoke managed organization with the slug to end management. Access stops immediately; the
management link is retained as an audit record.A new client org starts with no company
When somebody signs up for Erdo directly, we work out what their business is from the address they signed up with and research it, so their agents know who they are from the first message. A client org you provision has no such address — it is created by your manager credential, not by a person — so Erdo does not guess. The org starts with no company, and the marketing research that normally runs during onboarding waits rather than researching the wrong business. Tell Erdo who the customer is instead. Your portal already knows more about them than a domain lookup would find, so write it into the org as knowledge —erdo_create_knowledge, or POST /v1/knowledge
with the client’s org in X-Organization-ID. Their agents read it on every thread from then on. A
human member you add to the org can also give their company domain during their own onboarding, and
Erdo will research it the usual way.
Manager team access
The manager key is a credential, not a person, so on its own it gets your software into a client org and nobody on your team. Admins and owners of the manager organization therefore reach the orgs it manages directly: every actively managed org appears in their org switcher alongside their own, and they can open it and work in it without anyone adding them to it first. Three things bound that:- Admin, never owner. However senior you are in the manager org, derived authority stops at admin — the same ceiling the manager key itself has. Ownership of a client org stays with the client, so deleting the org and handing ownership on remain out of reach.
- Admins and owners only. A plain member of the manager org derives nothing. It is the same bar that decides who can provision or revoke a managed org at all.
- One hop. If a client org is itself a manager of others, your admins reach the client, not the orgs beyond it.
Reading client data from the manager organization
Switching into a client org is right for working on that client. It is the wrong shape for anything that spans them: a dashboard comparing five developments’ cost per lead, or a scheduled script that rolls spend up across them, runs in the manager organization, and an ordinary read from there could not see a client’s data no matter what its admins could reach by switching. So a client’s dataset purposes are what the manager reads through. A purpose is an organization’s own statement of which dataset holds which kind of data —leads, page events — and every dataset a
client declares one for becomes readable from the manager organization. A scratch analysis or a
half-finished import declares nothing, and stays private to the client that made it.
Start from the vocabulary, which lists every client’s purposes at once:
GET /v1/datasets-purposes?scope=managed,
the scope parameter on the erdo_list_dataset_purposes MCP tool, and datasets.purposes({ scope: "managed" })
inside a script. A page or a scheduled automation living in the manager organization can query a
client’s purpose dataset the same way — it is an ordinary dataset read, addressed by the client’s slug.
Two things bound it. The manager’s own dataset list stays its own datasets: this opens reading a
client’s dataset by name, not merging every client’s tables into one browse view. And the access ends
when the relationship does — revoking a client withdraws what management granted, while anything you
were deliberately shared stays yours.
Revoking is also the only way to end it. Removing the manager organization from a purpose dataset’s
sharing list does not stick while the client still declares that purpose and the relationship is still
live: the access comes from the relationship, and it is restored. That is not a way in that the
relationship did not already provide — the manager’s admins can read the same rows by switching into
the client organization — so ending the reach means ending the management link.
Providing an integration to your clients
Some of the tools your clients’ work depends on are ones you pay for rather than ones they have an account with — a people-lookup vendor an enrichment step calls on every lead, an enrichment or data API your automations are built around. The client has no relationship with that vendor and never will, so the only way to make the key usable in their organization used to be to connect the same key by hand, once per client, and to do it all again on the day you rotate it. Until that happened every step naming the app reported it as not connected and quietly did without. Instead, connect the key once, in your manager organization, and mark it as provided to the organizations you manage. From then on every client you manage resolves it as if it were its own: its agents get the credential at run time, the app appears in the set of things that organization can reach, and it is listed on its Connectors page. Nothing is copied anywhere — the reach is read from the live management links on every use, so a client you link next month picks it up with nothing to do, and revoking a client takes the key back by the same act that ends every other kind of reach. Mark an existing connection, using the integration’s id from your connections list:- Key-authenticated connections only. An API key, a database password, a service-account file — something your organization holds. An OAuth connection is a named person’s own account at the provider, so lending it would spend one individual’s authorization on work they never saw; asking to is refused rather than ignored.
- The client’s own connection always wins. Providing adds an app a client could not otherwise reach; it never displaces one that client connected for itself.
- It is not the client’s to change. A provided connection is read-only where it appears in the client’s Connectors page — the key belongs to your organization, and rotating or disconnecting it is your call, made once, for everyone.
- An admin or owner of the organization holding the key decides. Having connected it is not enough on its own: this decides which other businesses spend it.
Adding people to a managed org
A freshly provisioned client org has no human members — its only admin is the manager’s service identity. To give a person access (your own operators, or the customer’s team), add them by email from the manager surface:email and an optional role — member (the default) or admin. owner is
deliberately not grantable: a manager administers a client org, ownership stays with the client.
- If an Erdo account already exists for the email, it becomes an active member immediately
(
"invited": false) and can open the org in the app right away. - If no account exists yet, a pending invitation is created and an invite email is sent
(
"invited": true); the membership activates when they sign up with that address.
One page domain across your clients
A client org publishes its pages on Erdo’s pages host until a hostname is registered for it, and registering one per client means a DNS record and a certificate for each — repeated setup for what is one branded name. Instead, register a single custom domain on your manager org and mark it the default for the organizations you manage: every client you manage publishes on it, including ones you adopt or provision afterwards, with nothing to do per client. That hostname also serves those clients’ pages, so the URLs it mints and the pages it answers on are the same set. A client that registers its own domain keeps it — the manager default fills in where a client has none, and never overrides one. Both the branded URLs and the serving come from the live management relationship, so revoking it returns that client to Erdo’s pages host with nothing to unwind. The setup, the limits, and the API and CLI calls are on custom domains.Provisioning an ads container
If your manager org’s Google Ads connection is a manager account (MCC), you can give a managed org its own Google Ads container — a fresh sub-account under your MCC, plus a delegatedgoogle_ads
connection inside the managed org that operates it — in one call:
descriptive_name (the account’s display name in Google Ads) defaults to
<org name> (<org slug>), currency_code to USD, and time_zone to America/New_York — note
that Google fixes an account’s currency and time zone at creation. The response identifies the
container, never any credential material:
customer_id is the managed org’s own Google Ads account; login_customer_id is your MCC, the
context every request against that account runs in. The delegated connection stores both, so agents
and syncs inside the managed org operate the sub-account directly — no per-org browser OAuth.
That connection is a resource of the client organization rather than of yours, so it appears in that
org’s own integrations list — GET /v1/integrations with X-Organization-ID set to the client —
next to anything the client’s own team has connected.
It belongs to the client organization rather than to the person who provisioned it, too. Nobody gets
personal ownership of it by making the call: the client org owns the connection, its own admins
govern it from there, and your reach comes from the management link — so ending that link ends your
access to it, which would not be true of a grant held in somebody’s name. If a client wants your org
to keep standing access to the connection independently of the link, one of their admins can share
it with you explicitly, and that share survives everything here.
Billing is the one step left to a human
A new sub-account is created with no billing setup, and until it has one nothing it runs will serve. Google only lets the API attach a billing setup for accounts on monthly invoicing, so for a card-billed MCC somebody has to link a payments profile in the Google Ads UI.billing_configured
and billing_status say where the account stands, so you can surface that step while the campaigns
are being built rather than discovering it at go-live.
billing_status carries Google’s own value — APPROVED, PENDING, APPROVED_HELD, CANCELLED —
plus two of ours: none when the account has no billing setup at all, and unknown when the check
could not be made. Those last two are different facts and worth treating differently: none means
somebody has to go and link a payments profile, unknown means we could not reach Google to find
out. Only APPROVED sets billing_configured to true.
Both fields are read live, and there is a read-only way to ask for them — GET the same path:
404; one that has it answers the body above with
"already_provisioned": true and billing as Google reports it at that moment. The read resolves the
same management relationship and the same MCC as the create, so the two can never disagree about
which container an org has, and it creates nothing. The POST does report both fields on a repeat
call, so an idempotent create is not wrong — but a portal refreshing an org’s billing status, or
deciding whether to offer provisioning at all, is asking a question, and the GET is the way to ask
it without sending a request that is allowed to create an account.
The 404 is a definitive “this org has no container”, which is why it is not what you get while one
is being created. A read that lands during a provisioning call answers 503 — not settled yet, ask
again — rather than telling a poller there is nothing here moments before there is.
Two preconditions, both reported as errors rather than guessed around: your manager org must hold an
active Google Ads connection, and that connection must be an MCC one (it carries a
login_customer_id — stored automatically when the token’s only accessible account is a manager, or
set explicitly on the connection’s credentials). The call is idempotent: an org that already has
a delegated connection under your MCC gets it back with "already_provisioned": true, and a second
sub-account is never created — even when a previous attempt failed part-way.
One container per managed org is the point: each client’s campaigns, audiences, and conversion data
live in an account only that org’s connection operates, instead of several clients sharing one
account behind scope filters.
An organization that already runs campaigns
An organization’s Google Ads calls resolve through its newest active connection, so the delegated connection provisioning creates becomes the account that organization operates from then on. Google Ads has no operation that moves a campaign between accounts, so campaigns already running do not come with it — they keep serving, and keep spending, in an account nothing in the organization can reach any more. Nothing errors. Provisioning therefore counts before it creates. It resolves the account the org’s existing connection operates and counts the campaigns in it that Google has not removed; more than zero and the call is refused, naming the organization, the account, how many campaigns are in it and how many of those are enabled. The two numbers answer different questions — the total is the size of the rebuild, the enabled count is whether money is moving through that account right now. An org that has never connected Google Ads, or whose connection names no account, has nothing to strand and provisions normally. A connection that needs reauthorizing counts too. Google Ads knows nothing about the state of an OAuth token — the account keeps serving and keeps spending — so an org whose Google Ads connection has expired or been revoked is an org with a live account, not an org without one. The call is refused, naming the account and saying the connection cannot currently be read, so you can reauthorize and ask again or consent to leaving that account behind. The same rule applies to reading a container: one whose connection needs reauthorizing is still reported as the org’s container rather than as none, so a second account is never created for an org that already has one. A count is only worth taking if nothing can change underneath it, so for the few seconds the call runs — the count, the account creation at Google, the new connection — that organization’s Google Ads calls are held rather than answered. They come back as a retryable “being replaced right now, try again” instead of quietly acting on the account that is about to be superseded, and a second provisioning call for the same organization is refused rather than creating a second sub-account. The hold covers the other side of the same question too: for those seconds, somebody in the client org connecting Google Ads, reconnecting it, or switching which account a connection operates is asked to try again rather than allowed to change the answer the count was taken from. Nothing else is affected — the hold names one app in one organization, and it ends when the call does."replace_current_account": true on the body is your consent to leave those campaigns behind, and
the response then reports previous_customer_id and previous_campaign_count so the record says
what was left where. Treat it as a rebuild rather than a migration: campaigns, ad groups, keywords,
negative keyword lists and conversion actions all live in the account they were created in, so every
one of them has to be made again in the new account, and the performance history the old account
accumulated stays with the old account.
Adopting an existing organization
Provisioning covers orgs you create; adoption brings an org that already exists under your management — one created before your manager org existed, or previously run standalone. Because adoption hands a manager admin access to everything in the org, it requires two-sided consent: your manager key alone can never take over an org. Both consents can come from one person or two. When you own the organization yourself — you run the manager org and created the client org under your own account — your owner role is the consent, and no token changes hands. Adopt existing on Settings → Manager Accounts lists the organizations you own that can be adopted; pick one and it joins your client list in a single step. Programmatically the same shortcut is{ "organization_slug": "..." } in the adopt call below — it only works for a signed-in person who
is a seated owner of that organization, never for the manager key.
When someone else owns it, the owner offers and you accept:
1
The client's owner offers the organization
Every member of an organization sees who operates it in the This organization’s manager
section of that page (shown on organizations that are not themselves manager accounts), so a
client can always tell whether it is managed and by whom. When the organization has no manager
and the person looking is an owner, the page offers
Offer this organization for management. One click mints a one-time token and shows it in
the same dialog, with its expiry and a copy button — exactly once. The owner hands it to you
out of band; treat it as a secret, since whoever redeems it gains admin access.Owners only, and a signed-in person only: adoption grants the manager admin access to everything
in the organization, so an admin of it cannot make that call and an API key cannot make it at
all. The token works once and expires after 7 days. If the organization already has a manager,
minting is refused — that manager must revoke its link first, because an organization has one
manager at a time.
2
You adopt it
On the manager side of the same page, Adopt existing sits beside Create organisation at
the top of the client organizations list. Paste the token, and the organization joins your
client list; from then
on your manager key operates it exactly like one you provisioned, by naming its slug in
X-Organization-ID.POST /organization-adoption-invite on the signed-in session surface — an API key
cannot reach that surface, which is what stops a manager credential minting its own consent —
optionally carrying { "manager_org_slug": "your-manager-org" } to pin the redeemer to one named
manager. The response contains the raw token, returned exactly once because only its hash is stored.
You redeem it with the same authority as every other managed-org call: admin or owner of the manager
org, or the manager key itself.
X-Organization-ID — exactly like a provisioned org. The
response is the same shape as create: the org’s slug, name, and your management role.
Adoption changes who operates the org, not what’s in it: datasets, pages, knowledge, members,
and ownership are untouched — the client’s owner stays the owner. A used, expired, or unknown token
reads uniformly as not found, so a leaked or guessed token confirms nothing. Redeeming a token for
an org you already manage succeeds as a no-op; stopping management afterwards is the same
revoke managed organization as ever, and the org can later be adopted again with a fresh token.
Handoff tokens
An org you provisioned has no owner — its only admin is your manager’s service identity, and ownership is deliberately never grantable over the manager surface. The adoption flow above therefore cannot run for it: there is no owner to mint the consent, so without a second path such an org would stay pinned to whichever manager created it for good. A handoff token is that second path, minted by the organization’s current manager rather than by an owner. The consent still holds, and it is worth being explicit about why. For an ownerless managed org the current manager is the organization’s entire existing authority, so handing it on grants the successor nothing the minter did not already hold — the minter is relinquishing access, not conferring it. That argument stops applying the moment the organization has a real owner, so the mint refuses from then on: re-parenting an owned organization is the owner’s call, through the adoption invite above, once the current manager has revoked its link. The check runs again at redeem, in case an owner was seated during the token’s life. Mint the token against the organization you are handing over, naming the manager taking it on:successor_manager_slug is required, and must name an organization other than yours: a bearer token
that transfers control of an organization always names its recipient, which keeps a leaked one
useless to anyone else. As with an adoption token, the raw value comes back exactly once — only its
hash is stored — it works once, and it expires after 7 days.
The successor redeems it at the endpoint adoption already uses,
POST /v1/managed-organization-adoptions with { "token": "..." }, or with
erdo org managed adopt <token> on the CLI. Redeeming swaps the management link in a single step, so
the organization is never briefly managed by both of you or by neither: your link is revoked and your
service identity’s membership removed — exactly the state revoking leaves, audit record and all —
while the successor’s link and admin membership are written. Your key stops working in that
organization immediately.
A handoff token is only good while you are still the organization’s active manager. If you revoke the
link, or management moves some other way, redeeming reads as an ordinary bad token — the same “not
found, expired, or already used” any invalid token gets, so it confirms nothing about what the token
once was. That is also how you cancel a handoff you have changed your mind about: redeem your own
token, which spends it and leaves the management link exactly where it is.
Erdo platform
Settings → Manager Accounts in the Erdo platform is the front door for both sides of the relationship, and it is available to any Erdo organisation — being a manager is something an ordinary org takes on, not a separate kind of account. The page reads differently depending on which side of the relationship the organisation is on. A manager account — any org with client organisations — opens with the client list: create a client organisation, or adopt an existing organization (pick one of your own from the list, or paste a token); on each client, add a member by email — the result says whether the person was seated immediately or sent an invitation, because no Erdo account existed for that address yet — or open the client organisation to work inside it. Stopping management and creating or rotating the manager key live here too; the raw key is shown once, so copy it into your server-side secret manager before leaving the page. An organisation that is not a manager account instead shows This organization’s manager, visible to every member: whether the org is operated by a manager account, which organisation that is, and when the link was created. Being told who operates the organisation you work in is not privileged information, and those members already see the manager’s service identity in the team list. An owner of an unmanaged org also gets Offer this organization for management there, which mints the adoption token described above and shows it once with its expiry. Creating, adopting, revoking, adding members, and rotating the key require an admin or owner of the manager org, and offering your own organisation for management requires an owner of it.CLI
erdo login, erdo org use). In CI, set
the manager key as ERDO_API_KEY and pin the target tenant with ERDO_ORG or --org. See the
CLI reference.
MCP tools
Identity and RBAC ride the request context — a caller only ever acts as their own manager org, and
managed orgs are referenced by slug, never a UUID.
Providing an integration to your clients is deliberately not an MCP tool either, for the same reason:
widening the set of organizations a credential serves is a decision a person makes, on the Platform,
the CLI, or
POST /v1/integrations/:integrationID/managed-access.
Manager keys are deliberately not minted by an MCP tool: a model-facing tool must not create a
non-expiring credential. Create or rotate the key from Platform, the CLI, or POST /v1/manager-key,
then use that credential to operate managed orgs through MCP. Member adds stay off the model-facing
tool surface for the same reason — grant people access via POST /v1/managed-organizations/:slug/members —
and so do adoption and handoff, because durably re-homing an organization is a decision a person makes
on the Platform, the CLI, or the REST endpoints above.
REST
Base URLhttps://api.erdo.ai. Authenticate with Authorization: Bearer <token> and select the
manager org with X-Organization-ID (for the lifecycle calls, that’s your manager org; to operate a
managed org, set it to that org’s slug/id).
The manager key returned by
POST /v1/manager-key is a standard erdo_api_* bearer token pinned to
your manager org — send it as Authorization: Bearer <key> with X-Organization-ID: <managed-org-slug>
to act inside any org you manage.
