Skip to main content

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-ID header (or erdo --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.
Because access is enforced by membership plus the org header — the same mechanics as any Erdo token — there is no new auth path to reason about. A managed org’s service usage never counts toward your manager org’s billable seats, and never appears in its Team list. Erdo pauses syncs and scheduled automations in organizations nobody is using, and a managed org counts as in use while you are: as long as someone from your manager org is signing in, your clients’ data syncs, monitors, and refreshes keep running, even for a client org you never open directly. If Erdo had auto-paused a client refresh, a manager sign-in resumes it and clears its pause notice; explicitly disabled jobs remain disabled. Your manager organization’s own refreshes still require viewing their output or an explicit resume; signing in alone does not wake those refreshes.

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.
Only an admin/owner of the manager org can create, revoke, or mint the key — the same authority check on every surface below. Listing is available to any member of the manager org.

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.
Nobody gains a seat this way: derived access creates no membership in the client org, so those people do not count towards the client’s billable seats and do not appear in its Team list. Ending management ends the access with it — within a few minutes, as the access decision is cached briefly. When somebody needs to be a real member of a client org rather than reach it through the relationship, add them below.

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:
Each row names the client organization, the purpose, and the dataset that holds it. Those dataset slugs are organization-qualified, so you can read them straight from the manager organization — nothing to switch, nothing to copy between tenants:
The same read is available wherever the vocabulary already was: 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:
Run it from your manager organization. Naming a client while you are working inside a different client answers “organization not found”, because the account you are acting in is what tells Erdo which management relationship — and so whose campaign-ownership snapshot — the read should use. Switch to the manager organization and ask again. Reading your own organization needs membership of it, so an API token scoped to particular resources cannot ask this question: it reports on an organization as a whole, across every dataset behind it, which is wider than any resource grant such a token carries. It answers with one row per organization per calendar day (UTC), carrying: 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 the campaign 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:
Or say it while connecting the key in the first place:
Either way the answer names the organizations the key now serves, because deciding which businesses may spend a credential you pay for is worth seeing before anything runs. A manager organization that has not linked a client yet gets an empty list rather than a refusal — the setting stands, and the first client you link uses it. Four things bound it, and they are worth being explicit about:
  • 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.
Stop providing it with the same command and the other value:

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:
The id is the one 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:
The body takes 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.
Re-adding an existing member is idempotent, and never demotes: requesting a lower role than the one they already hold reports the role in effect.

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 delegated google_ads connection inside the managed org that operates it — in one call:
The body is optional. 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. 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:
An org with no container under your MCC answers 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.
What you get back is the same container the create path produces — the delegated connection inside the managed org, operating that account under your MCC — with 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.
One case is deliberately not a refusal. The census that protects live campaigns asks whether the org’s existing connection is running campaigns in an account it is about to lose — and when that connection already operates the very account being adopted, nothing is lost. Adoption is then recording what the organization already runs, so it goes ahead without 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 runs provision_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.
Management never chains: an organization that manages clients of its own cannot be adopted or offered, and a managed organization cannot create client organizations — one level, by design, so handing an org to a manager never transitively hands over anything else. The same flow is available without the screens, for a portal that drives it programmatically. The owner’s mint is 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.
Redeeming records the management link and seats the manager’s service identity in the org, so the manager key can operate it immediately via 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

The CLI authenticates with your Erdo token and active org (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 URL https://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.

Reconnecting Google Ads for managed organizations

Managed Google Ads connections keep their own client account selection and use the manager connection’s current authorization. Reconnect Google Ads once in the manager organization to update access for every client using that connection. Grant the Data Manager permission during reconnect to enable offline-conversion uploads for those clients. Tokens and permissions are not copied into each client. Ending the management relationship or disconnecting the manager’s connection stops that access.