curl --request POST \
--url https://api.erdo.ai/v1/managed-organizations/{orgSlug}/ads-container \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"currency_code": "<string>",
"customer_id": "<string>",
"descriptive_name": "<string>",
"replace_current_account": true,
"time_zone": "<string>"
}
'import requests
url = "https://api.erdo.ai/v1/managed-organizations/{orgSlug}/ads-container"
payload = {
"currency_code": "<string>",
"customer_id": "<string>",
"descriptive_name": "<string>",
"replace_current_account": True,
"time_zone": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
currency_code: '<string>',
customer_id: '<string>',
descriptive_name: '<string>',
replace_current_account: true,
time_zone: '<string>'
})
};
fetch('https://api.erdo.ai/v1/managed-organizations/{orgSlug}/ads-container', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.erdo.ai/v1/managed-organizations/{orgSlug}/ads-container",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'currency_code' => '<string>',
'customer_id' => '<string>',
'descriptive_name' => '<string>',
'replace_current_account' => true,
'time_zone' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.erdo.ai/v1/managed-organizations/{orgSlug}/ads-container"
payload := strings.NewReader("{\n \"currency_code\": \"<string>\",\n \"customer_id\": \"<string>\",\n \"descriptive_name\": \"<string>\",\n \"replace_current_account\": true,\n \"time_zone\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.erdo.ai/v1/managed-organizations/{orgSlug}/ads-container")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"currency_code\": \"<string>\",\n \"customer_id\": \"<string>\",\n \"descriptive_name\": \"<string>\",\n \"replace_current_account\": true,\n \"time_zone\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.erdo.ai/v1/managed-organizations/{orgSlug}/ads-container")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"currency_code\": \"<string>\",\n \"customer_id\": \"<string>\",\n \"descriptive_name\": \"<string>\",\n \"replace_current_account\": true,\n \"time_zone\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"adopted": true,
"already_provisioned": true,
"billing_configured": true,
"billing_status": "<string>",
"customer_id": "<string>",
"descriptive_name": "<string>",
"integration_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"login_customer_id": "<string>",
"org_slug": "<string>",
"previous_campaign_count": 123,
"previous_customer_id": "<string>"
}{
"code": "not_found",
"details": {},
"message": "<string>"
}ProvisionManagedOrganizationAdsContainerAPI gives a managed org its own Google Ads container in
one call: a fresh sub-account under the MANAGER org’s Google Ads manager account (MCC), plus a delegated google_ads connection in the managed org that operates it. Idempotent — an org that already has a delegated connection under this MCC gets it back (already_provisioned: true), never a second sub-account. Deliberately no MCP tool: creating a billable provider account and a durable delegated credential stays off the model-facing tool surface, like manager-key minting and member adds (see the file header).
curl --request POST \
--url https://api.erdo.ai/v1/managed-organizations/{orgSlug}/ads-container \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"currency_code": "<string>",
"customer_id": "<string>",
"descriptive_name": "<string>",
"replace_current_account": true,
"time_zone": "<string>"
}
'import requests
url = "https://api.erdo.ai/v1/managed-organizations/{orgSlug}/ads-container"
payload = {
"currency_code": "<string>",
"customer_id": "<string>",
"descriptive_name": "<string>",
"replace_current_account": True,
"time_zone": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
currency_code: '<string>',
customer_id: '<string>',
descriptive_name: '<string>',
replace_current_account: true,
time_zone: '<string>'
})
};
fetch('https://api.erdo.ai/v1/managed-organizations/{orgSlug}/ads-container', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.erdo.ai/v1/managed-organizations/{orgSlug}/ads-container",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'currency_code' => '<string>',
'customer_id' => '<string>',
'descriptive_name' => '<string>',
'replace_current_account' => true,
'time_zone' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.erdo.ai/v1/managed-organizations/{orgSlug}/ads-container"
payload := strings.NewReader("{\n \"currency_code\": \"<string>\",\n \"customer_id\": \"<string>\",\n \"descriptive_name\": \"<string>\",\n \"replace_current_account\": true,\n \"time_zone\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.erdo.ai/v1/managed-organizations/{orgSlug}/ads-container")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"currency_code\": \"<string>\",\n \"customer_id\": \"<string>\",\n \"descriptive_name\": \"<string>\",\n \"replace_current_account\": true,\n \"time_zone\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.erdo.ai/v1/managed-organizations/{orgSlug}/ads-container")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"currency_code\": \"<string>\",\n \"customer_id\": \"<string>\",\n \"descriptive_name\": \"<string>\",\n \"replace_current_account\": true,\n \"time_zone\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"adopted": true,
"already_provisioned": true,
"billing_configured": true,
"billing_status": "<string>",
"customer_id": "<string>",
"descriptive_name": "<string>",
"integration_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"login_customer_id": "<string>",
"org_slug": "<string>",
"previous_campaign_count": 123,
"previous_customer_id": "<string>"
}{
"code": "not_found",
"details": {},
"message": "<string>"
}Authorizations
An Erdo API key (erdo_api_...) or scoped token (erdo_token_...).
Path Parameters
Body
the org's container instead of creating a new sub-account — for an account somebody already created by hand in the Google Ads UI. Google cannot delete an account, so adopting is the only way to avoid leaving a duplicate behind. The account must be an enabled, non-manager, direct client of the MCC that no other org you manage already holds.
account being left behind. An org already running campaigns somewhere else cannot be moved into a fresh container — Google Ads has no way to move a campaign between accounts — so provisioning refuses by default rather than repointing the org at an empty account while the spend carries on where the org can no longer see it. Passing this says those campaigns will be rebuilt, and the response then names the account and the campaign count that were left behind.
Response
Success response
existed under the manager's MCC (named by the request's customer_id) and was connected rather than created.
delegated connection under this manager's MCC, which was returned instead of creating a second sub-account.
setup, which is what decides whether it can serve. A container is created with none: Google's BillingSetupService can only attach one programmatically for customers on monthly invoicing, so until the MCC is approved for that, somebody has to link a payments profile in the Google Ads UI. Saying so here is the difference between a known remaining step and one discovered at go-live, when the campaigns are built and nothing serves.
APPROVED_HELD, CANCELLED. Two values are ours rather than Google's: "none" when the account has no billing setup at all, and "unknown" when the check itself could not be made. Those are different facts and a caller gating go-live must not read the second as the first — an unreadable answer is a reason to go and look, not evidence of absence.
under the manager's MCC).
Empty when AlreadyProvisioned: Erdo does not store Google's display name, and re-reading it would put a Google call on the idempotent path.
It is the id every request against CustomerID is sent in the context of, and it names the manager DIRECTLY above the sub-account, which is not always the account the manager organization's own connection logs in as: a manager reached through a parent MCC holds its clients itself, and a container under it is addressed through it rather than through the parent.
carrying — the size of the rebuild. Zero means the account it superseded had nothing in it, which is why no consent was needed to supersede it.
already operating when this container was provisioned, if it was operating one. That account is where its campaigns still are — nothing moved them, and no connection in the org resolves it any more.

