curl --request POST \
--url https://api.cloudhumans.com/cloudchat/v1/accounts/{accountId}/portals \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--header 'cloudchat-instance: <cloudchat-instance>' \
--data '
{
"portal": {
"name": "Acme Help Center",
"slug": "acme-help",
"default_locale": "pt_BR"
}
}
'import requests
url = "https://api.cloudhumans.com/cloudchat/v1/accounts/{accountId}/portals"
payload = { "portal": {
"name": "Acme Help Center",
"slug": "acme-help",
"default_locale": "pt_BR"
} }
headers = {
"cloudchat-instance": "<cloudchat-instance>",
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {
'cloudchat-instance': '<cloudchat-instance>',
Authorization: 'Bearer <token>',
'Content-Type': 'application/json'
},
body: JSON.stringify({portal: {name: 'Acme Help Center', slug: 'acme-help', default_locale: 'pt_BR'}})
};
fetch('https://api.cloudhumans.com/cloudchat/v1/accounts/{accountId}/portals', 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.cloudhumans.com/cloudchat/v1/accounts/{accountId}/portals",
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([
'portal' => [
'name' => 'Acme Help Center',
'slug' => 'acme-help',
'default_locale' => 'pt_BR'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json",
"cloudchat-instance: <cloudchat-instance>"
],
]);
$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.cloudhumans.com/cloudchat/v1/accounts/{accountId}/portals"
payload := strings.NewReader("{\n \"portal\": {\n \"name\": \"Acme Help Center\",\n \"slug\": \"acme-help\",\n \"default_locale\": \"pt_BR\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("cloudchat-instance", "<cloudchat-instance>")
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.cloudhumans.com/cloudchat/v1/accounts/{accountId}/portals")
.header("cloudchat-instance", "<cloudchat-instance>")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"portal\": {\n \"name\": \"Acme Help Center\",\n \"slug\": \"acme-help\",\n \"default_locale\": \"pt_BR\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.cloudhumans.com/cloudchat/v1/accounts/{accountId}/portals")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["cloudchat-instance"] = '<cloudchat-instance>'
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"portal\": {\n \"name\": \"Acme Help Center\",\n \"slug\": \"acme-help\",\n \"default_locale\": \"pt_BR\"\n }\n}"
response = http.request(request)
puts response.read_body{
"id": 9,
"slug": "acme-help",
"name": "Acme Help Center",
"default_locale": "pt_BR",
"allowed_locales": [
"pt_BR"
],
"archived": false,
"created_at": "2026-08-19T18:02:11.004Z",
"updated_at": "2026-08-19T18:02:11.004Z"
}{
"error": {
"code": "bad_request",
"message": "The request is malformed."
}
}{
"error": {
"code": "unauthorized",
"message": "Authentication is required. Send a valid Bearer token in the Authorization header."
}
}{
"error": {
"code": "forbidden",
"message": "You are not allowed to perform this action."
}
}{
"error": {
"code": "not_found",
"message": "Resource could not be found."
}
}{
"error": {
"code": "validation_failed",
"message": "The request payload is invalid.",
"details": [
{
"field": "short_code",
"code": "taken",
"message": "has already been taken"
}
]
}
}{
"message": "API rate limit exceeded"
}{
"error": {
"code": "internal_error",
"message": "An unexpected error occurred. Please try again later."
}
}Create a portal
Create a new help center site on the account. This is the first step of building one entirely through the API: create the portal, allow the locales you need, create categories, then create articles.
The portal is created already reachable by visitors, but empty: only published articles are ever served and articles start as drafts, so nothing leaks while you fill it in. Its allowed_locales starts as exactly the default_locale you send, which matters immediately — a category can only exist in an allowed locale, so a second language needs an update first.
Requires being an administrator of the account.
curl --request POST \
--url https://api.cloudhumans.com/cloudchat/v1/accounts/{accountId}/portals \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--header 'cloudchat-instance: <cloudchat-instance>' \
--data '
{
"portal": {
"name": "Acme Help Center",
"slug": "acme-help",
"default_locale": "pt_BR"
}
}
'import requests
url = "https://api.cloudhumans.com/cloudchat/v1/accounts/{accountId}/portals"
payload = { "portal": {
"name": "Acme Help Center",
"slug": "acme-help",
"default_locale": "pt_BR"
} }
headers = {
"cloudchat-instance": "<cloudchat-instance>",
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {
'cloudchat-instance': '<cloudchat-instance>',
Authorization: 'Bearer <token>',
'Content-Type': 'application/json'
},
body: JSON.stringify({portal: {name: 'Acme Help Center', slug: 'acme-help', default_locale: 'pt_BR'}})
};
fetch('https://api.cloudhumans.com/cloudchat/v1/accounts/{accountId}/portals', 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.cloudhumans.com/cloudchat/v1/accounts/{accountId}/portals",
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([
'portal' => [
'name' => 'Acme Help Center',
'slug' => 'acme-help',
'default_locale' => 'pt_BR'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json",
"cloudchat-instance: <cloudchat-instance>"
],
]);
$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.cloudhumans.com/cloudchat/v1/accounts/{accountId}/portals"
payload := strings.NewReader("{\n \"portal\": {\n \"name\": \"Acme Help Center\",\n \"slug\": \"acme-help\",\n \"default_locale\": \"pt_BR\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("cloudchat-instance", "<cloudchat-instance>")
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.cloudhumans.com/cloudchat/v1/accounts/{accountId}/portals")
.header("cloudchat-instance", "<cloudchat-instance>")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"portal\": {\n \"name\": \"Acme Help Center\",\n \"slug\": \"acme-help\",\n \"default_locale\": \"pt_BR\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.cloudhumans.com/cloudchat/v1/accounts/{accountId}/portals")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["cloudchat-instance"] = '<cloudchat-instance>'
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"portal\": {\n \"name\": \"Acme Help Center\",\n \"slug\": \"acme-help\",\n \"default_locale\": \"pt_BR\"\n }\n}"
response = http.request(request)
puts response.read_body{
"id": 9,
"slug": "acme-help",
"name": "Acme Help Center",
"default_locale": "pt_BR",
"allowed_locales": [
"pt_BR"
],
"archived": false,
"created_at": "2026-08-19T18:02:11.004Z",
"updated_at": "2026-08-19T18:02:11.004Z"
}{
"error": {
"code": "bad_request",
"message": "The request is malformed."
}
}{
"error": {
"code": "unauthorized",
"message": "Authentication is required. Send a valid Bearer token in the Authorization header."
}
}{
"error": {
"code": "forbidden",
"message": "You are not allowed to perform this action."
}
}{
"error": {
"code": "not_found",
"message": "Resource could not be found."
}
}{
"error": {
"code": "validation_failed",
"message": "The request payload is invalid.",
"details": [
{
"field": "short_code",
"code": "taken",
"message": "has already been taken"
}
]
}
}{
"message": "API rate limit exceeded"
}{
"error": {
"code": "internal_error",
"message": "An unexpected error occurred. Please try again later."
}
}Authorizations
The id_token from POST /auth/v1/signin, sent as Authorization: Bearer <id_token>. Not the access_token — that one does not carry the identity Cloud Chat authorizes on.
Headers
Your Cloud Chat instance ID — an integer, fixed for your company, told at onboarding. The API overview explains how instances work, how to find yours, and the errors a wrong or missing value produces.
1
Path Parameters
Your Cloud Chat account. It has to be an account your token grants membership on, and it has to live on the instance in the cloudchat-instance header — the two travel together. Account numbers are only unique within an instance, so the same number is a different company on another instance. Usually a mismatched pair fails closed with a 401, because your user does not exist on the other instance — but if your identity happens to exist on both, the call succeeds against the other company's data, silently. Read it and you are looking at the wrong help center; write it and you have stored into the wrong account. Send the two values that were given to you together, and never try a number to see what answers.
1
Body
The portal goes under a portal wrapper. The contract is deliberately small — a logo, more locales and a custom domain are set afterwards, the first two by updating the portal and the last one only in the dashboard.
Show child attributes
Show child attributes
Response
The portal as stored.
A help center site of the account.
Numeric id, informational only — every help center endpoint addresses the portal by slug.
3
What goes in the portalSlug path segment of the other help center endpoints.
"acme-help"
Display name of the help center.
"Acme Help Center"
Locale used when nothing more specific applies — including as the translation target for an article without a category.
"en"
Locales the portal can hold content in. A category (and through it, an article) must live in one of these.
["en", "pt_BR"]
An archived portal is no longer served to visitors.
false
"2026-05-02T11:04:17.000Z"
"2026-08-10T14:32:05.123Z"