Subscribe a Client to a Plan
curl --request POST \
--url https://app.tuco.ai/api/agency/billing/subscribe \
--header 'Authorization: Bearer <token>'import requests
url = "https://app.tuco.ai/api/agency/billing/subscribe"
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/billing/subscribe', 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/billing/subscribe",
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/billing/subscribe"
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/billing/subscribe")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.tuco.ai/api/agency/billing/subscribe")
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
Subscribe a Client to a Plan
Put a client workspace on a Tuco plan, charged to your agency card — one call, no checkout page.
POST
/
api
/
agency
/
billing
/
subscribe
Subscribe a Client to a Plan
curl --request POST \
--url https://app.tuco.ai/api/agency/billing/subscribe \
--header 'Authorization: Bearer <token>'import requests
url = "https://app.tuco.ai/api/agency/billing/subscribe"
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/billing/subscribe', 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/billing/subscribe",
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/billing/subscribe"
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/billing/subscribe")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.tuco.ai/api/agency/billing/subscribe")
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_bodyThis is not a hosted-checkout redirect. The subscription is created directly
against your agency’s saved card, so a script can run it. It was the last step of
client onboarding that needed a browser.
Endpoint
- Method:
POST - Path:
/api/agency/billing/subscribe - Auth:
Authorization: Bearer tucoagency_xxxxxxxxxxxxx(agency key)
Body
| Field | Type | Required | Notes |
|---|---|---|---|
workspaceId | string | yes | A client workspace in your agency |
plan | string | yes | starter or growth. Mini is retired for agencies |
curl https://app.tuco.ai/api/agency/billing/subscribe \
-H "Authorization: Bearer tucoagency_xxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"workspaceId":"org_3KKJKupHJ8Y9I2NXVAga0c6ZqgT","plan":"starter"}'
{ "success": true, "subscriptionId": "sub_1QX…", "status": "active", "plan": "starter" }
You need a card on file first
The charge goes to the agency owner’s saved payment method, off-session. If there isn’t one, you get400 no_card — add it from Agency view → Billing, then
retry. Any agency member (or key) can subscribe a workspace; the charge always
lands on the owner’s card, so nobody can mis-bill anyone else.
The setup fee, if your agency pays one, lands on the subscription’s first invoice
together with the first month — the same site pricing a direct customer pays,
minus whatever discount is attached to your agency.
If Tuco invoices your agency directly
Agencies on invoice billing don’t need a card at all. The same call puts the client on a plan, creates no Stripe subscription, and charges nothing — Tuco bills your agency offline for the whole group.{ "success": true, "subscriptionId": null, "status": "active", "plan": "growth", "billingMode": "invoice" }
subscriptionId is null and the client workspace gets no stripeCustomerId.
That absence is the point: your clients cannot be dunned, emailed about a
failed payment, or sent to a checkout page, because they do not exist in Stripe
at all. Add-on lines work the same way — the slot is granted and appears on
your agency’s next invoice.
Invoice billing is set by Tuco on your agency account, not self-serve. Ask us.
Why the plan is still required either way
It’s tempting to read “we invoice our own clients” as “no subscription record at all”. That breaks the product: a workspace’s plan is what grants its entitlements.organizations.plan sets how many phone and email lines it may
hold, and an agency workspace left at subscriptionStatus: 'none' is refused
outright by the line guard — it cannot order a line, and a transfer into it is
rejected with “Target workspace has no active subscription”.
So subscribe every client, in either mode. A plan is an entitlement, not a
charge. What you control is the price: on Stripe billing that’s your
agency’s discount coupon; on invoice billing it’s whatever we agreed. Either
way, what you charge your client on your own invoice is separate and Tuco never
sees it.
Error responses
| Status | Code | When |
|---|---|---|
400 | no_card | No saved payment method on the agency. Not possible on invoice billing — no card is needed |
400 | already_subscribed | That workspace already has a subscription |
400 | — | Missing workspaceId/plan, or a retired plan |
401 | UNAUTHORIZED | Missing, unknown, revoked or expired key |
403 | AGENCY_KEY_REQUIRED | A workspace key (tuco_…) was used |
403 | — | That workspace is not part of your agency |