Create Agency Workspace
curl --request POST \
--url https://app.tuco.ai/api/agency/subaccounts \
--header 'Authorization: Bearer <token>'import requests
url = "https://app.tuco.ai/api/agency/subaccounts"
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
fetch('https://app.tuco.ai/api/agency/subaccounts', 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://app.tuco.ai/api/agency/subaccounts",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://app.tuco.ai/api/agency/subaccounts"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://app.tuco.ai/api/agency/subaccounts")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.tuco.ai/api/agency/subaccounts")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_bodyAgency
Create Agency Workspace
Create a new client workspace inside your Tuco agency, subscribe it to a plan, and get its workspace API key back — in one call.
POST
/
api
/
agency
/
subaccounts
Create Agency Workspace
curl --request POST \
--url https://app.tuco.ai/api/agency/subaccounts \
--header 'Authorization: Bearer <token>'import requests
url = "https://app.tuco.ai/api/agency/subaccounts"
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
fetch('https://app.tuco.ai/api/agency/subaccounts', 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://app.tuco.ai/api/agency/subaccounts",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://app.tuco.ai/api/agency/subaccounts"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://app.tuco.ai/api/agency/subaccounts")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.tuco.ai/api/agency/subaccounts")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_bodyCreates a workspace under your agency, optionally subscribes it to a plan,
and returns a ready-to-use workspace API key for it — so an onboarding
script never has to stop and mint a key or make a second billing call by hand.
Endpoint
- Method:
POST - Path:
/api/agency/subaccounts - Auth:
Authorization: Bearer tucoagency_xxxxxxxxxxxxx(agency key)
Body
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Display name, max 120 characters |
legalName | string | no | The client’s legal/LLC name, max 200 characters |
timezone | string | no | IANA zone (e.g. America/Denver). Sets the daily-cap reset window. Invalid zones are rejected with 400 |
plan | string | no | starter or growth. Subscribes the workspace in the same call — see below. Omit to create it unsubscribed |
Example request
curl https://app.tuco.ai/api/agency/subaccounts \
-H "Authorization: Bearer tucoagency_xxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Roofing",
"legalName": "Acme Roofing LLC",
"timezone": "America/Denver",
"plan": "growth"
}'
Success response (200)
{
"success": true,
"subAccount": {
"clerkOrgId": "org_3KKLH9HaVf4KyI6TiNhQk4T0cfa",
"name": "Acme Roofing",
"agencyId": "user_3H08IRicrVUYLDZRyOh4jKRVwrx",
"isAgencyWorkspace": true
},
"workspaceApiKey": "tuco_xCsfhTiXXXXXXXXXXXXXXXXXXXXXXXX",
"workspaceApiKeyWarning": "Save this workspace key now. You will not be able to see it again.",
"plan": "growth",
"subscriptionStatus": "active",
"billingMode": "invoice",
"planError": null
}
plan. Omit it and the
response is exactly what it always was.
workspaceApiKey is shown once. Store it when you create the workspace — it
cannot be retrieved later, only replaced from the workspace’s Integrations page.workspaceApiKey is null — make one from the workspace’s Integrations page.
Subscribing in the same call
A plan is not optional in practice.plan is what grants a workspace its line
limits, and an agency workspace with no plan is refused outright — a line
transfer into one fails. So every onboarding script made the second call anyway.
plan on create collapses the two.
| Plan | |
|---|---|
starter | $149/mo + one-time setup |
growth | Higher line limits |
There is no
enterprise plan. email-only (Mini) exists but is retired and is
rejected for new subscriptions. Workspaces already on it keep renewing.POST /api/agency/billing/subscribe,
so the behaviour matches exactly:
- Invoiced agency — the workspace gets the plan and no card is charged.
billingMode: "invoice", and no Stripe subscription exists for it, which is what makes your client structurally unreachable by dunning and payment mail. Line caps do not apply either — see below. - Stripe agency — your saved card is charged off-session, as it would be on
the standalone route.
billingMode: "stripe".
An invalid plan is rejected before anything is created
{
"error": "Unknown plan \"enterprise\".",
"code": "INVALID_PLAN",
"validPlans": ["starter", "growth"]
}
400, and no workspace exists — the plan is validated before the workspace
is created, so a typo cannot leave an orphan behind.
A billing failure does not lose the workspace
If the plan cannot be applied after the workspace exists — a declined card, Stripe unreachable — you still get200, the workspace, and its key:
{
"success": true,
"subAccount": { "clerkOrgId": "org_3KKLH9HaVf4KyI6TiNhQk4T0cfa", "…": "…" },
"workspaceApiKey": "tuco_xCsfhTiXXXXXXXXXXXXXXXXXXXXXXXX",
"plan": null,
"subscriptionStatus": "none",
"planError": "Your card was declined."
}
planError, then retry the billing step alone
with POST /api/agency/billing/subscribe
— do not create the workspace again.
If Tuco invoices your agency, lines are uncapped
Plan line limits exist to meter what a card gets charged for. When Tuco invoices your agency offline for the whole group, there is nothing for them to meter — they can only block a line you have already agreed to pay for. So for an invoiced agency, every client workspace can order any number of lines, whatever plan it carries. It applies to all three paths — ordering a line, creating one directly, and the capacity the dashboard reports, which returnsunlimitedReason: "invoiced_agency".
This is a commercial arrangement, not a setting. Tuco switches it on per agency,
the same switch that enables line-health webhooks.
On Stripe billing, normal plan caps apply and extra lines are add-on purchases.
plan: it is what sets the workspace’s message throughput and
what marks it active. It just stops being a ceiling on lines.
What the new workspace starts as
- Owned by your agency (
isAgencyWorkspace: true) — it has no card of its own and bills through you. subscriptionStatus: "active"when you passed a plan,"none"otherwise. Subscribe it before moving lines in; a transfer into an unsubscribed workspace is refused.- You and your agency co-admins are admins on it, so it appears in the workspace switcher immediately.
- It has its own workspace API key, returned above.
Error responses
| Status | Code | When |
|---|---|---|
400 | — | name missing, or timezone is not a valid IANA zone |
400 | INVALID_PLAN | Unknown plan. The body lists validPlans |
400 | RETIRED_PLAN | plan: "email-only" — Mini is retired for new subscriptions |
401 | UNAUTHORIZED | Missing, unknown, revoked or expired key |
403 | AGENCY_KEY_REQUIRED | A workspace key (tuco_…) was used |
403 | NOT_AN_AGENCY | The account behind the key is not an agency |