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.

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.

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. Two things bound it. The manager’s own dataset list stays its own datasets: this opens reading a client’s dataset by name, not merging every client’s tables into one browse view. And the access ends when the relationship does — revoking a client withdraws what management granted, while anything you were deliberately shared stays yours. Revoking is also the only way to end it. Removing the manager organization from a purpose dataset’s sharing list does not stick while the client still declares that purpose and the relationship is still live: the access comes from the relationship, and it is restored. That is not a way in that the relationship did not already provide — the manager’s admins can read the same rows by switching into the client organization — so ending the reach means ending the management link.

Providing an integration to your clients

Some of the tools your clients’ work depends on are ones you pay for rather than ones they have an account with — a people-lookup vendor an enrichment step calls on every lead, an enrichment or data API your automations are built around. The client has no relationship with that vendor and never will, so the only way to make the key usable in their organization used to be to connect the same key by hand, once per client, and to do it all again on the day you rotate it. Until that happened every step naming the app reported it as not connected and quietly did without. Instead, connect the key once, in your manager organization, and mark it as provided to the organizations you manage. From then on every client you manage resolves it as if it were its own: its agents get the credential at run time, the app appears in the set of things that organization can reach, and it is listed on its Connectors page. Nothing is copied anywhere — the reach is read from the live management links on every use, so a client you link next month picks it up with nothing to do, and revoking a client takes the key back by the same act that ends every other kind of reach. Mark an existing connection, using the integration’s id from your connections list:
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:

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 rolemember (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.

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 your MCC, the context every request against that account runs in. The delegated connection stores both, so agents and syncs inside the managed org operate the sub-account directly — no per-org browser OAuth. That connection is a resource of the client organization rather than of yours, so it appears in that org’s own integrations list — GET /v1/integrations with X-Organization-ID set to the client — next to anything the client’s own team has connected. It belongs to the client organization rather than to the person who provisioned it, too. Nobody gets personal ownership of it by making the call: the client org owns the connection, its own admins govern it from there, and your reach comes from the management link — so ending that link ends your access to it, which would not be true of a grant held in somebody’s name. If a client wants your org to keep standing access to the connection independently of the link, one of their admins can share it with you explicitly, and that share survives everything here.

Billing is the one step left to a human

A new sub-account is created with no billing setup, and until it has one nothing it runs will serve. Google only lets the API attach a billing setup for accounts on monthly invoicing, so for a card-billed MCC somebody has to link a payments profile in the Google Ads UI. billing_configured and billing_status say where the account stands, so you can surface that step while the campaigns are being built rather than discovering it at go-live. billing_status carries Google’s own value — APPROVED, PENDING, APPROVED_HELD, CANCELLED — plus two of ours: none when the account has no billing setup at all, and unknown when the check could not be made. Those last two are different facts and worth treating differently: none means somebody has to go and link a payments profile, unknown means we could not reach Google to find out. Only APPROVED sets billing_configured to true. Both fields are read live, and there is a read-only way to ask for them — GET the same path:
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 be an MCC one (it carries a login_customer_id — stored automatically when the token’s only accessible account is a manager, or set explicitly on the connection’s credentials). The call is idempotent: an org that already has a delegated connection under your MCC gets it back with "already_provisioned": true, and a second sub-account is never created — even when a previous attempt failed part-way. One container per managed org is the point: each client’s campaigns, audiences, and conversion data live in an account only that org’s connection operates, instead of several clients sharing one account behind scope filters.

An organization that already runs campaigns

An organization’s Google Ads calls resolve through its newest active connection, so the delegated connection provisioning creates becomes the account that organization operates from then on. Google Ads has no operation that moves a campaign between accounts, so campaigns already running do not come with it — they keep serving, and keep spending, in an account nothing in the organization can reach any more. Nothing errors. Provisioning therefore counts before it creates. It resolves the account the org’s existing connection operates and counts the campaigns in it that Google has not removed; more than zero and the call is refused, naming the organization, the account, how many campaigns are in it and how many of those are enabled. The two numbers answer different questions — the total is the size of the rebuild, the enabled count is whether money is moving through that account right now. An org that has never connected Google Ads, or whose connection names no account, has nothing to strand and provisions normally. A connection that needs reauthorizing counts too. Google Ads knows nothing about the state of an OAuth token — the account keeps serving and keeps spending — so an org whose Google Ads connection has expired or been revoked is an org with a live account, not an org without one. The call is refused, naming the account and saying the connection cannot currently be read, so you can reauthorize and ask again or consent to leaving that account behind. The same rule applies to reading a container: one whose connection needs reauthorizing is still reported as the org’s container rather than as none, so a second account is never created for an org that already has one. A count is only worth taking if nothing can change underneath it, so for the few seconds the call runs — the count, the account creation at Google, the new connection — that organization’s Google Ads calls are held rather than answered. They come back as a retryable “being replaced right now, try again” instead of quietly acting on the account that is about to be superseded, and a second provisioning call for the same organization is refused rather than creating a second sub-account. The hold covers the other side of the same question too: for those seconds, somebody in the client org connecting Google Ads, reconnecting it, or switching which account a connection operates is asked to try again rather than allowed to change the answer the count was taken from. Nothing else is affected — the hold names one app in one organization, and it ends when the call does. "replace_current_account": true on the body is your consent to leave those campaigns behind, and the response then reports previous_customer_id and previous_campaign_count so the record says what was left where. Treat it as a rebuild rather than a migration: campaigns, ad groups, keywords, negative keyword lists and conversion actions all live in the account they were created in, so every one of them has to be made again in the new account, and the performance history the old account accumulated stays with the old account.

Adopting an existing organization

Provisioning covers orgs you create; adoption brings an org that already exists under your management — one created before your manager org existed, or previously run standalone. Because adoption hands a manager admin access to everything in the org, it requires two-sided consent: your manager key alone can never take over an org. Both consents can come from one person or two. When you own the organization yourself — you run the manager org and created the client org under your own account — your owner role is the consent, and no token changes hands. Adopt existing on Settings → Manager Accounts lists the organizations you own that can be adopted; pick one and it joins your client list in a single step. Programmatically the same shortcut is { "organization_slug": "..." } in the adopt call below — it only works for a signed-in person who is a seated owner of that organization, never for the manager key. When someone else owns it, the owner offers and you accept:
1

The client's owner offers the organization

Every member of an organization sees who operates it in the This organization’s manager section of that page (shown on organizations that are not themselves manager accounts), so a client can always tell whether it is managed and by whom. When the organization has no manager and the person looking is an owner, the page offers Offer this organization for management. One click mints a one-time token and shows it in the same dialog, with its expiry and a copy button — exactly once. The owner hands it to you out of band; treat it as a secret, since whoever redeems it gains admin access.Owners only, and a signed-in person only: adoption grants the manager admin access to everything in the organization, so an admin of it cannot make that call and an API key cannot make it at all. The token works once and expires after 7 days. If the organization already has a manager, minting is refused — that manager must revoke its link first, because an organization has one manager at a time.
2

You adopt it

On the manager side of the same page, Adopt existing sits beside Create organisation at the top of the client organizations list. Paste the token, and the organization joins your client list; from then on your manager key operates it exactly like one you provisioned, by naming its slug in X-Organization-ID.
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.