Update user plan
curl --request PUT \
--url https://app.autocalls.ai/api/white-label/users/{id}/plan \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"plan_id": 123,
"minutes": "<string>",
"when_short": "<string>",
"billing": "<string>",
"ends_at": "<string>"
}
'import requests
url = "https://app.autocalls.ai/api/white-label/users/{id}/plan"
payload = {
"plan_id": 123,
"minutes": "<string>",
"when_short": "<string>",
"billing": "<string>",
"ends_at": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
plan_id: 123,
minutes: '<string>',
when_short: '<string>',
billing: '<string>',
ends_at: '<string>'
})
};
fetch('https://app.autocalls.ai/api/white-label/users/{id}/plan', 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.autocalls.ai/api/white-label/users/{id}/plan",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'plan_id' => 123,
'minutes' => '<string>',
'when_short' => '<string>',
'billing' => '<string>',
'ends_at' => '<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://app.autocalls.ai/api/white-label/users/{id}/plan"
payload := strings.NewReader("{\n \"plan_id\": 123,\n \"minutes\": \"<string>\",\n \"when_short\": \"<string>\",\n \"billing\": \"<string>\",\n \"ends_at\": \"<string>\"\n}")
req, _ := http.NewRequest("PUT", 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.put("https://app.autocalls.ai/api/white-label/users/{id}/plan")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"plan_id\": 123,\n \"minutes\": \"<string>\",\n \"when_short\": \"<string>\",\n \"billing\": \"<string>\",\n \"ends_at\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.autocalls.ai/api/white-label/users/{id}/plan")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"plan_id\": 123,\n \"minutes\": \"<string>\",\n \"when_short\": \"<string>\",\n \"billing\": \"<string>\",\n \"ends_at\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"message": "Plan saved.",
"data": {
"plan_id": 5,
"name": "Starter",
"source": "manual",
"billing": "monthly",
"included_minutes": 100,
"included_credits": 0,
"minutes_renew_at": "2026-11-02T14:25:38+00:00",
"ends_at": null,
"minutes": "monthly",
"when_short": "auto_top_up",
"renewal": {
"status": "ok",
"next_at": "2026-11-02T14:25:38+00:00",
"missing_minutes": 0,
"last_renewed_at": null,
"prepaid_minutes": null,
"prepaid_until": null
}
}
}
{
"message": "Unauthenticated."
}
{
"message": "You are not an administrator."
}
{
"message": "You do not have access to these settings."
}
{
"message": "User not found."
}
{
"message": "Insufficient balance: this plan needs 100 minutes and you have 88.83."
}
{
"message": "This user already has a plan paid in the platform. It must end before you can give one."
}
{
"message": "The selected plan does not belong to your platform.",
"errors": {
"plan_id": ["The selected plan does not belong to your platform."]
}
}
{
"message": "The selected minutes is invalid.",
"errors": {
"minutes": ["The selected minutes is invalid."]
}
}
{
"message": "The ends at field must be a date after today.",
"errors": {
"ends_at": ["The ends at field must be a date after today."]
}
}
{
"message": "The price field is not supported.",
"errors": {
"price": ["The price field is not supported."]
}
}
{
"message": "Too Many Attempts."
}
White Label
Update user plan
Give one of your users a subscription plan they pay you for outside your platform
PUT
/
white-label
/
users
/
{id}
/
plan
Update user plan
curl --request PUT \
--url https://app.autocalls.ai/api/white-label/users/{id}/plan \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"plan_id": 123,
"minutes": "<string>",
"when_short": "<string>",
"billing": "<string>",
"ends_at": "<string>"
}
'import requests
url = "https://app.autocalls.ai/api/white-label/users/{id}/plan"
payload = {
"plan_id": 123,
"minutes": "<string>",
"when_short": "<string>",
"billing": "<string>",
"ends_at": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
plan_id: 123,
minutes: '<string>',
when_short: '<string>',
billing: '<string>',
ends_at: '<string>'
})
};
fetch('https://app.autocalls.ai/api/white-label/users/{id}/plan', 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.autocalls.ai/api/white-label/users/{id}/plan",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'plan_id' => 123,
'minutes' => '<string>',
'when_short' => '<string>',
'billing' => '<string>',
'ends_at' => '<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://app.autocalls.ai/api/white-label/users/{id}/plan"
payload := strings.NewReader("{\n \"plan_id\": 123,\n \"minutes\": \"<string>\",\n \"when_short\": \"<string>\",\n \"billing\": \"<string>\",\n \"ends_at\": \"<string>\"\n}")
req, _ := http.NewRequest("PUT", 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.put("https://app.autocalls.ai/api/white-label/users/{id}/plan")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"plan_id\": 123,\n \"minutes\": \"<string>\",\n \"when_short\": \"<string>\",\n \"billing\": \"<string>\",\n \"ends_at\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.autocalls.ai/api/white-label/users/{id}/plan")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"plan_id\": 123,\n \"minutes\": \"<string>\",\n \"when_short\": \"<string>\",\n \"billing\": \"<string>\",\n \"ends_at\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"message": "Plan saved.",
"data": {
"plan_id": 5,
"name": "Starter",
"source": "manual",
"billing": "monthly",
"included_minutes": 100,
"included_credits": 0,
"minutes_renew_at": "2026-11-02T14:25:38+00:00",
"ends_at": null,
"minutes": "monthly",
"when_short": "auto_top_up",
"renewal": {
"status": "ok",
"next_at": "2026-11-02T14:25:38+00:00",
"missing_minutes": 0,
"last_renewed_at": null,
"prepaid_minutes": null,
"prepaid_until": null
}
}
}
{
"message": "Unauthenticated."
}
{
"message": "You are not an administrator."
}
{
"message": "You do not have access to these settings."
}
{
"message": "User not found."
}
{
"message": "Insufficient balance: this plan needs 100 minutes and you have 88.83."
}
{
"message": "This user already has a plan paid in the platform. It must end before you can give one."
}
{
"message": "The selected plan does not belong to your platform.",
"errors": {
"plan_id": ["The selected plan does not belong to your platform."]
}
}
{
"message": "The selected minutes is invalid.",
"errors": {
"minutes": ["The selected minutes is invalid."]
}
}
{
"message": "The ends at field must be a date after today.",
"errors": {
"ends_at": ["The ends at field must be a date after today."]
}
}
{
"message": "The price field is not supported.",
"errors": {
"price": ["The price field is not supported."]
}
}
{
"message": "Too Many Attempts."
}
This endpoint gives one of your users one of your plans, for customers who pay you outside your platform, for example by bank transfer, with your own payment links or on invoice. The user gets the plan’s limits and minute rate, and its included minutes if you choose so, and sees the plan on their plans page as managed by you, without the buttons to change or cancel it. It is the Plan action on the Users page of your administration panel.
Unknown fields are refused, and then nothing is saved.
This endpoint is available to the account owner (with an active plan) and to team members with Billing Access. Other users receive a 403 Forbidden error. Only users of your platform can be changed: any other ID, including the owner account, returns 404.
Minutes
The plan’s minutes come from the owner’s balance, with its chat credits at 9 credits per minute (thecost_in_minutes of List plans). With minutes you choose how:
none: only the plan, its limits and minute rate. Nothing is taken from the owner’s balance and nothing renews, for example when you move minutes yourself with Transfer balance.monthly: giving the plan takes one month of minutes and chat credits. Every month the user is topped back up to the plan’s minutes and gets its chat credits again, and the owner’s balance pays only for the minutes they used.yearly: giving the plan takes 12 months at once, and the user gets them month by month. Nothing more is taken for a year. After 12 months, what is left goes back to the owner’s balance and the next 12 months are taken.
- Moving the user to another plan or
minutesoption first returns the unused minutes of the current plan, and what is left of a yearly prepayment, to the owner’s balance. - Sending the user’s current plan with the same
minuteschanges onlybilling,ends_atandwhen_short. No minutes move. - Minutes the user had before, outside a plan you gave, stay with them. Chat credits already given stay with the user.
Renewals
Renewals run every hour for the plans whose renewal day has come. When the owner’s balance cannot cover one:- The renewal waits, and it is tried again every hour. The user keeps the plan, its limits and the minutes they have left. Get user plan shows
renewal.statusaspending, with the minutes missing. - The owner gets a notification in the administration panel and an email, at most once a day.
- With
when_shortset toauto_top_up, the owner’s auto top-up starts, with the amount and card set on the Credits page, even when the balance is above its threshold. It starts at most once a day. Withwait, the renewal waits for you to top up. - After a top-up, Renew user plan renews right away instead of within the hour.
- A plan whose last day comes on or before its renewal day is not renewed again: it ends on that day.
Path Parameters
integer
required
The ID of the user. Administrators can list user IDs with Get platform users.
Request Body
integer
required
One of your plans, from List plans. Inactive plans can be given too.
string
default:"monthly"
none, monthly or yearly, as described above. Left out, a plan you gave before keeps its option, and a new plan is monthly.string
default:"wait"
auto_top_up or wait: what happens when the owner’s balance cannot cover a renewal, as described above. Left out, a plan you gave before keeps its choice, and a new plan waits. Plans with minutes set to none never renew, so it does not apply to them.string
default:"monthly"
With
minutes set to none only: monthly or yearly, which of the plan’s prices the user sees as theirs. With monthly or yearly minutes, the user sees the matching price. If the plan has no price for it, its other price is used. Left out, a plan you gave before keeps its billing, and a new plan is monthly.string
The last day of the plan, as
YYYY-MM-DD, after today. The plan ends at the end of that day, and its unused minutes go back to the owner’s balance. Left out, a plan you gave before keeps its end date, and a new plan has none. Send null to remove the end date, so the plan stays until you remove it.Response
string
Plan saved.object
The user’s plan, as in Get user plan
{
"message": "Plan saved.",
"data": {
"plan_id": 5,
"name": "Starter",
"source": "manual",
"billing": "monthly",
"included_minutes": 100,
"included_credits": 0,
"minutes_renew_at": "2026-11-02T14:25:38+00:00",
"ends_at": null,
"minutes": "monthly",
"when_short": "auto_top_up",
"renewal": {
"status": "ok",
"next_at": "2026-11-02T14:25:38+00:00",
"missing_minutes": 0,
"last_renewed_at": null,
"prepaid_minutes": null,
"prepaid_until": null
}
}
}
{
"message": "Unauthenticated."
}
{
"message": "You are not an administrator."
}
{
"message": "You do not have access to these settings."
}
{
"message": "User not found."
}
{
"message": "Insufficient balance: this plan needs 100 minutes and you have 88.83."
}
{
"message": "This user already has a plan paid in the platform. It must end before you can give one."
}
{
"message": "The selected plan does not belong to your platform.",
"errors": {
"plan_id": ["The selected plan does not belong to your platform."]
}
}
{
"message": "The selected minutes is invalid.",
"errors": {
"minutes": ["The selected minutes is invalid."]
}
}
{
"message": "The ends at field must be a date after today.",
"errors": {
"ends_at": ["The ends at field must be a date after today."]
}
}
{
"message": "The price field is not supported.",
"errors": {
"price": ["The price field is not supported."]
}
}
{
"message": "Too Many Attempts."
}
Rate limit
The platform settings, default limits, user settings and plan endpoints share a limit of 60 requests per minute for each account. Above it they return429, and the Retry-After response header tells you how many seconds to wait.
Example: Give a plan with monthly minutes
curl -X PUT https://app.autocalls.ai/api/white-label/users/123/plan \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"plan_id": 5,
"minutes": "monthly",
"when_short": "auto_top_up"
}'
Example: A year paid upfront
curl -X PUT https://app.autocalls.ai/api/white-label/users/123/plan \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"plan_id": 5,
"minutes": "yearly"
}'
Example: Only the plan’s limits, until the end of the year
curl -X PUT https://app.autocalls.ai/api/white-label/users/123/plan \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"plan_id": 5,
"minutes": "none",
"billing": "yearly",
"ends_at": "2026-12-31"
}'

