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. Removing a human member from a manager organization also removes their background jobs’ access to client datasets through that organization. When a dataset is transferred, its former manager’s automatic access is withdrawn before ownership changes; an explicit share remains in place.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.
Three things bound it. It is for the people who administer the manager organization — admins and
owners, the same people who can switch into a client — so a plain member of the manager organization
reads its own data and no client’s, and asking for the cross-client vocabulary tells them so rather
than returning an empty list. Automations the manager organization runs, such as a scheduled rollup,
keep working: they act as the organization rather than as a member of 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. Moving a
dataset into a different organization moves the access with it: the old client’s manager stops
reading it, and the new organization’s manager starts.
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.
Performance across your clients
Reading each client’s datasets is the right shape for a one-off question. It is the wrong shape for a dashboard, because assembling one means holding Erdo’s own column names — which timestamp a lead is dated by, which table a provider’s spend lands in — in your code. Those are Erdo’s to change, and when they changed, a dashboard built that way reported a client’s leads short with no error anywhere. So there is one typed read that answers the dashboard question directly:leads_on_page and leads_captured count different things on purpose: the first is what the browser
saw, the second is what arrived. A gap between them is a capture problem worth looking at, so they are
reported side by side rather than reconciled into one number.
Each organization also carries campaigns — the same spend broken out by the provider’s own campaign
id, one row per day and campaign — and coverage.
A missing measurement is null, never zero
coverage is the part to read first. A client that has connected no advertising account and a client
whose advertising dataset could not be read both have unknown spend, and reporting either as $0
states something untrue: a customer connected on 7 September had a month of real spend shown as zero
for exactly that reason.
So every measurement is nullable. When a source cannot be read, its fields are null on every day and
coverage carries an entry naming the dataset role that would have answered and why it did not —
no dataset carries that purpose, two datasets carry it and which one holds the data is a reconciliation
rather than a lookup, or the dataset itself could not be read. The CLI prints those fields as - and
lists the reasons under the table.
That holds for a partial answer too. If a client’s own advertising account reads fine but the
campaigns it runs in an account you share could not be resolved, its spend comes back null rather
than the smaller total — a partial total looks exactly like a complete one, and nothing in it would
say the client had spent more.
It holds day by day for advertising as well. Spend, impressions, clicks and conversions are only
reported for the days a client’s advertising sync has loaded in full: if that sync last completed on
the 12th, the 13th onward come back null with a coverage entry naming the date, rather than as zero
days you would read as a client who stopped spending. A connection whose first sync has not finished
reports no advertising numbers at all, for the same reason.
The most recent day is held back on the same principle. A sync runs at a moment, and the provider
reports against its account’s own calendar, so the day a sync was still inside had only part of its
spend fetched — sometimes none of it, when the account’s date is behind UTC. That day is reported as
null rather than as the fraction that happened to be loaded. Days further back are unaffected: a
client who has genuinely stopped spending still measures zero, because those days were fetched and
came back empty. And a sync that failed on the advertising table itself while succeeding elsewhere
reports no advertising numbers at all, rather than the older rows it left behind.
Where the numbers come from
Each client’s own dataset purposes resolve the sources:leads, page events, and ad spend. Nothing here asks you to name a dataset.
A managed Google Ads account establishes ad spend when its account reporting feed
is ready to read. The regular reporting check also repairs a missing purpose on an
existing ready feed. It leaves an incomplete feed unbound and reports a conflict
if another dataset already carries that purpose, including a retired source.
Changing which source owns the purpose remains an explicit correction.
Leads are dated by whichever arrival timestamp the client’s dataset actually carries, oldest-known
first, so a client whose lead capture predates a column Erdo added later is still counted rather than
silently dropped.
Advertising spend is the one measurement that is not on a UTC day: a provider reports it against its
own account-timezone calendar date, and that is the date used, so a day’s spend and a day’s leads can
be a few hours out of step at the boundary.
Campaigns in an account you share across clients
If your clients each have their own advertising account, the read needs nothing more. If several of them run in one account you operate, Erdo cannot tell whose campaign is whose — so you say, by publishing a dataset under thecampaign ownership purpose in your manager organization holding a
snapshot_json column. The snapshot names, for each provider account, the campaigns each client owns
and the dataset that account can be read through. Those campaigns’ spend is then attributed to the
client that owns them, marked shared_account, and everything out of a client’s own account is marked
own_account.
That holds even when the client syncs its own copy of the shared account. Its dataset still contains
every client’s campaigns, so the read is filtered to the campaigns your snapshot claims for that client
rather than summing the table — and those rows are marked shared_account too, because the claim on
them is yours. If the snapshot claims none of that account’s campaigns for the client whose dataset it
is, the spend comes back null with a coverage entry: summing the account would report another
client’s money, and reporting nothing would hide spend the client really has.
Each attributed campaign row carries your own evidence string back unchanged, so you can treat one
kind of claim differently from another — a campaign claimed because it points at that client’s page is
evidence about today’s funnel, while one claimed from a historical declaration may be spend whose
traffic never reached that client’s pages. Erdo does not interpret the value; it reports it so a
cost-per-lead can leave out what it should.
Seven rules bound it, and each one exists so a mistake in that dataset cannot become a wrong answer.
Only an outright ownership claim attributes spend, and a campaign your own reconciliation marked
ambiguous does more than drop out: that whole account’s spend comes back null with a coverage entry,
because reading only the campaigns you did claim would publish part of an account’s total as all of it.
A campaign your snapshot claims for two different clients refuses its account the same way: giving it
to both would report the same money twice across your portfolio, and giving it to one would pick a
winner your snapshot does not name. A snapshot older than 48 hours stops being evidence, and the
attribution is reported as unavailable instead. The newest snapshot you published is the one Erdo
checks, so a newer one written in a shape Erdo does not understand, or dated in the future, makes the
attribution unavailable rather than quietly falling back to an older snapshot. One dataset cannot be
the source for two different accounts — that contradiction makes the whole snapshot unavailable rather
than letting the last row read win. A source row whose provider_key disagrees with what that dataset
actually syncs is refused rather than read through, with both values in the coverage entry so you can
correct the row; otherwise the query would run against a table that cannot match and report no spend.
And the snapshot can only name organizations you actually manage and datasets inside that
relationship; anything else in it is ignored.
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.
When you connected the vendor inside a client organization first
Providing a key means the key has to be connected in your manager organization, and often it is not: the vendor was connected months ago, inside whichever client organization first needed it, before there was any thought of the others using it. Connecting it again in the manager organization sounds easy until you try — a vendor key is stored so that nobody can read it back, including you, so the only way to type it again is to fetch it again from the vendor’s own dashboard. If Erdo has recorded you as the current key’s authenticated supplier, CLI 0.85.0 can copy it from the existing connection inside Erdo, without displaying it:erdo integrations list --org <the client org holding it> prints. That single
command leaves the manager organization holding its own connection to the vendor, provided to every
client you manage — the key never passing through your hands on the way.
Only the person who supplied the key can do this — whoever connected it, or whoever last rotated it.
An admin of the organization holding the key can disconnect it, rotate it or share it, because those
are acts on that organization’s own infrastructure; taking a key that nobody can read into a different
organization is not, and it stays with whoever supplied it. A key a recipient entered through a
connect link has no supplier on your side, so it cannot be reused this way. The rest of the bounds are
the ones you would expect: a key-authenticated native integration, connected and working, and the same
app on both sides.
Legacy connections without a verified supplier cannot use this shortcut. The row’s creator or
an unchanged timestamp does not prove who supplied its current key. Supply the vendor key directly
to the manager connection, or replace the source key while signed in before reusing 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.One WhatsApp number across your clients
A manager can share one of its approved WhatsApp numbers with every organization it manages, so a client’s lead follow-up reaches people abroad before the client’s own number is on WhatsApp. Each person stays with one client at a time, decided by what the person last did. See Shared WhatsApp number.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 the manager
account it sits directly under, which is the context every request against it runs in. That is your
MCC in the ordinary case, and the sub-manager when the connection you provision from operates one
beneath your MCC — a container belongs under the manager your connection acts as, not under whatever
sits above it. 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.
If the manager connection’s selected account or credentials change while Google
is verifying the manager, provisioning returns a retryable error. Retry after the
connection change finishes so the container is created under the current manager.
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.
Advertiser verification decides whether ads show your name and logo
Google shows a business name and logo on an ad only after the advertiser has passed Google’s identity checks for that account. Until then it refuses to add either one. The account can still serve ads without them.advertiser_verification reports where each account stands:
The checks happen on Google’s page, and no API can complete them. To get a link,
POST the same
path with "start_advertiser_verification": true. If the account still needs verifying and has no
open session, Erdo starts one, and the response then carries action_url. It never starts a session
for an account that is verified, under review, or reports check_in_google_ads; Google exposes API
verification sessions only for monthly-invoiced accounts. A plain GET never starts one. Google limits how
often verification can be read, so a GET may return an answer up to 30 minutes old.
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 operate a manager account. Which manager a
connection runs in is settled when its account is chosen rather than inferred from what the login can
see: picking your MCC in the account picker records it, and picking an account the picker reached
through a manager records that manager alongside the account. You can also state it on the
connection directly. Whether that account really is a manager account is Google’s answer, asked
before anything is created under it — an ordinary advertiser account is refused by name rather than
producing a provider error nobody can act on, and a lookup that fails says so instead of claiming
your manager account is not one. A connection whose chosen account is an ordinary advertiser, or
which has several accounts chosen at once, provisions under the manager those accounts were reached
through, since that is the manager it is acting as. 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 account you created by hand
Google Ads has no operation that deletes an account, so every account is permanent. A sub-account somebody created in the Google Ads UI cannot be tidied away — and neither can a second one created beside it, so provisioning a fresh container for a client that already has a hand-made account leaves your MCC holding two accounts for one client for good, distinguishable only by their ids. Adoption is the way out of that: name the account that already exists and provisioning connects it instead of creating one.adopted saying which of the two
happened:
descriptive_name is the account’s own name at Google rather than one Erdo composed, because Erdo
did not name it. For the same reason the creation inputs do not apply: an account that exists already
has a name, a currency and a time zone, and Google fixes the last two at creation, so the CLI refuses
--name, --currency and --time-zone alongside --adopt rather than appearing to set something
that was never set.
Four checks stand between the id you pass and the container, and each of them refuses rather than
adjusting:
- A direct client of your MCC. The account must sit immediately under the manager the connection operates. Erdo’s authority over it comes from exactly that link, so an account further down someone else’s hierarchy is one it cannot operate for the org however real the id is.
- Enabled, and not itself a manager. A closed or cancelled account cannot serve, and a manager account holds no campaigns of its own — either would produce a container that reads as provisioned and can never run a campaign.
- Not already another organization’s account. If any other Erdo organization is operating that account, adoption is refused — through any connection it holds, not only through a container Erdo provisioned, since a client that connected Google Ads itself and picked the account is operating it just as surely. When that organization is one you manage, the refusal names it; when it is one you used to manage, it says so, because ending the relationship does not disconnect its Google Ads connection and the fix is to go and disconnect it rather than go and ask. An organization you do not manage is not named. One account per managed org is the point of provisioning, and two organizations pointing at one account is precisely the shared-account collision it exists to end. Two adoptions of the same account at the same moment are refused for the same reason, whichever managers they come from: the second waits rather than passing a check the first has not finished invalidating.
- The org holds no other container. An org that already has a container under a different id is refused rather than quietly repointed: the container is where its campaigns, conversion actions and performance history live, so moving it is the rebuild described above, not a change of address.
replace_current_account.
From inside the managed organization
Everything above is the manager’s surface and assumes whoever needs the account is holding the manager key. Often they are not. An agency admin working inside a client organization, building that client’s campaigns in chat, is exactly the person who discovers the org has no ad account of its own — and sending them out to a REST call in the middle of a build is a poor answer, so the agent in that organization can provision one. It rides Erdo’s own connected app rather than a tool of its own: the agent finds it the way it finds any integration action, by searching for what it is trying to do, and runsprovision_google_ads_account
on the erdo app. Searching for Google Ads actions surfaces it too, beside the campaign actions the
agent would go on to use. Its inputs are the REST body — customer_id to adopt an account that
exists, descriptive_name, currency_code and time_zone for one being created, and
replace_current_account to consent to leaving campaigns behind.
Two things bound it. The authority is the
manager’s, not the client’s: the person the run belongs to must be an admin or owner of the manager
organization that manages this one, resolved from inside the client org. A member of the client org
with no standing in the manager is refused, which is what keeps “give this org its own ad account”
from being something a client can do to your MCC. And an approval card comes first, naming the
account about to be created or adopted, the organization it is for, and the manager it will sit
under, so a person accepts a spend-bearing change rather than an agent making it in passing.
An organization can be operated by more than one manager, and then the account’s home is a real
question rather than a detail: it is created under that manager’s Google Ads manager account, with
that organization’s credential. So it is never guessed. When you administer more than one of the
managers operating the org, the call is refused and the refusal lists them; pass
manager_organization_slug to say which one you are acting as. With a single manager to choose
between — the ordinary case — nothing is asked and nothing has to be passed. From the manager side
(the REST and CLI calls above) the question does not arise at all: the manager is the organization
you are making the call from.
The result is the same container by the same route — the same delegated connection, in the same
organization, governed the same way. There is deliberately no MCP tool for it: creating an account at
a provider under your manager credential is not a capability a model-facing tool surface should offer
to anyone holding an API key, and the approval card is what makes the agent path a decision a person
makes rather than one a model can take alone.
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.

