Authentication
There are two ways to authenticate with Ahsuite:
- API Token (Basic auth) — for your own code: server scripts, cron jobs, Zapier-style tools and direct REST API calls. Use the credentials below.
- OAuth — for AI apps & third-party apps (Claude, ChatGPT, Cursor...). No token to copy: the app opens Ahsuite in your browser, you log in and click Approve. See Connect from an AI app below.
API Token (Basic auth)
Pass your Owner Email as the username and one of your API tokens as the password in the Authorization header. Tokens are shown only once when generated — if you lose one, revoke it and generate a new one. Never expose a token in client-side code or public repos.
Connect from an AI app (MCP)
Ahsuite is an MCP server — connect it once and your AI app can list portals, create tasks, start conversations and more, just by chatting. Authentication is OAuth: the app opens Ahsuite in your browser, you log in and click Approve. No token needed.
- claude.ai (web) — Settings → Connectors → Add custom connector → paste the URL above → Add → log in & Approve.
- Claude Desktop — Settings → Connectors → Add custom connector → same URL.
- ChatGPT — Settings → Connectors/Plugins (enable Developer mode) → Add custom connector → same URL.
- Any app that supports MCP (Cursor, Windsurf, VS Code...) — add Ahsuite as an MCP server using the URL above; the app will open the Ahsuite login & Approve flow automatically.
- Claude Code (terminal) — run the command below, then
/mcp→ Authenticate:
claude mcp add --transport http ahsuite https://ahsuite.com/api/mcp
Running from a server or CI where a browser login isn't possible? Use Basic auth on the same URL instead: Authorization: Basic base64(email:token).
OAuth for third-party apps
Building an app that connects to your users' Ahsuite accounts? Use OAuth 2.1 (authorization code + PKCE). Your app never sees the user's password or token — the user logs in on Ahsuite and approves access. The tokens you receive work on the whole REST API and the MCP endpoint.
1. Register your app (once)
curl -X POST https://ahsuite.com/oauth/register \
-H "Content-Type: application/json" \
-d '{"client_name":"My App","redirect_uris":["https://myapp.com/callback"]}'
Response includes your client_id. Redirect URIs must be https (or http://localhost for development).
2. Send the user to the consent screen
https://ahsuite.com/oauth/authorize?response_type=code &client_id=CLIENT_ID &redirect_uri=https://myapp.com/callback &state=RANDOM_STATE &code_challenge=PKCE_S256_CHALLENGE &code_challenge_method=S256 &resource=https://ahsuite.com/api/mcp
The user logs in and clicks Approve. Ahsuite redirects back to your redirect_uri with ?code=...&state=.... PKCE is required. resource declares which endpoint the token is for (RFC 8707) — required for MCP clients, optional for REST-only apps.
3. Exchange the code for tokens
curl -X POST https://ahsuite.com/oauth/token \ -d "grant_type=authorization_code&code=CODE&redirect_uri=https://myapp.com/callback&client_id=CLIENT_ID&code_verifier=PKCE_VERIFIER&resource=https://ahsuite.com/api/mcp"
Returns an access_token (valid 1 hour) and a refresh_token (valid 30 days, rotates on every use).
4. Call the API
curl https://ahsuite.com/api/portal/list \ -H "Authorization: Bearer ACCESS_TOKEN"
5. Refresh when the access token expires
curl -X POST https://ahsuite.com/oauth/token \ -d "grant_type=refresh_token&refresh_token=REFRESH_TOKEN&client_id=CLIENT_ID"
Machine-readable config is published at /.well-known/oauth-authorization-server — standard OAuth libraries can configure themselves from it. Users can disconnect your app anytime from Connected Apps.
Getting a 403 error?
A 403 response — on the REST API, the MCP endpoint or the OAuth flow — usually means one of these:
- Plan — API access requires a Professional or Agency plan.
- OAuth redirect — your
redirect_urimust behttps://. - Blocked by our system — repeated failed or unusual requests are blocked automatically.
If your plan and setup are correct and it keeps happening, contact us at sudhir@ahsuite.com.
Add a Portal
Create a new portal with a primary contact and optional custom variables. Returns the new client_id on success.
address.portal or user.delhi.0 to create a clean portal without your default portal template (blueprint). Omit or 1 = the default template is applied, same as creating a portal from the app.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
-F 'portal_name=Portal Name' \
-F 'contact_email=contact@example.com' \
-F 'contact_name=Contact Name' \
-F 'variables[0][name]=address' \
-F 'variables[0][value]=delhi' \
'https://ahsuite.com/api/portal/create'$ch = curl_init();
$data = [
'portal_name' => 'Portal Name here',
'contact_email' => 'contact@example.com',
'contact_name' => 'Contact Name here',
'variables[0][name]' => 'address',
'variables[0][value]' => 'delhi',
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/create');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const form = new URLSearchParams();
form.append('portal_name', 'Portal Name');
form.append('contact_email', 'contact@example.com');
form.append('contact_name', 'Contact Name');
form.append('variables[0][name]', 'address');
form.append('variables[0][value]', 'delhi');
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/create', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body: form,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/create',
auth=('EMAIL', 'TOKEN'),
data={
'portal_name': 'Portal Name',
'contact_email': 'contact@example.com',
'contact_name': 'Contact Name',
'variables[0][name]': 'address',
'variables[0][value]': 'delhi',
},
)
print(res.json()){
"success": true,
"message": "Portal has been added successfully.",
"data": {
"client_id": "vJW4"
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"portal_name\":[\"The portal name field is required.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}List Portals
Return the portals owned by the authenticated account, newest first. Only active portals are returned. The portal_id in each row is the same ID accepted by every other endpoint on this page. Results are paginated — use page and limit to walk through the full list.
1.20, maximum 100.Other unsuccessful response codes: 403, 429, 500.
curl --request GET \
-u "EMAIL:TOKEN" \
'https://ahsuite.com/api/portal/list?search=Acme&page=1&limit=20'$params = http_build_query([
'search' => 'Acme',
'page' => 1,
'limit' => 20,
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/list?'.$params);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const params = new URLSearchParams({ search: 'Acme', page: 1, limit: 20 });
const res = await fetch(`https://ahsuite.com/api/portal/list?${params}`, {
headers: { Authorization: `Basic ${auth}` },
});
console.log(await res.json());import requests
res = requests.get(
'https://ahsuite.com/api/portal/list',
params={'search': 'Acme', 'page': 1, 'limit': 20},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "",
"data": {
"portals": [
{
"name": "Acme Corp",
"created_date": "2026-05-12 10:04:33",
"portal_id": "vJW4"
},
{
"name": "Acme Digital",
"created_date": "2026-04-02 08:11:20",
"portal_id": "vJW3"
}
],
"pagination": {
"total": 42,
"page": 1,
"limit": 20,
"total_pages": 3
}
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"limit\":[\"The limit may not be greater than 100.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}Portal Detail
Return the full details of a single portal — name, contact info and the preference fields accepted by Update Portal Preferences, plus nested sidebar, client_user, account_manager, branding, email_notifications and variables sections. Every field uses the same name its settings update endpoint accepts. Requires a valid portal_id (get one from List Portals).
Other unsuccessful response codes: 403, 429, 500.
curl --request GET \
-u "EMAIL:TOKEN" \
'https://ahsuite.com/api/portal/detail?portal_id=vJW4'$params = http_build_query([
'portal_id' => 'vJW4',
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/detail?'.$params);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const params = new URLSearchParams({ portal_id: 'vJW4' });
const res = await fetch(`https://ahsuite.com/api/portal/detail?${params}`, {
headers: { Authorization: `Basic ${auth}` },
});
console.log(await res.json());import requests
res = requests.get(
'https://ahsuite.com/api/portal/detail',
params={'portal_id': 'vJW4'},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "",
"data": {
"name": "Acme Corp",
"phone": "+1 480 555 0117",
"website": "acmecorp.com",
"language": "en",
"timezone": "America/Phoenix",
"content_align": "align_left",
"two_step_auth": 0,
"portal_search": 1,
"files_no_login": 1,
"files_hide_preview": 0,
"created_date": "2026-05-12 10:04:33",
"portal_id": "vJW4",
"sidebar": {
"show_pages": 1, "show_files": 1, "show_password": 0, "show_tasks": 1, "show_conversations": 1, "show_projects": 1,
"label_files": "Files", "label_password": "", "label_tasks": "Tasks", "label_conversations": "", "label_projects": "",
"order": ["pages", "tasks", "conversations", "files", "password", "projects"],
"default_tab": "pages",
"links": [{ "name": "Our Website", "url": "https://example.com", "same": 0 }]
},
"client_user": { "allow_add_portal_users": 0, "email_receive": 1, "allow_page_add": 0 },
"account_manager": {
"show_card": 1, "show_avatar": 1, "show_name": 1, "show_title": 1, "show_email": 1, "show_phone": 0,
"display_name": "", "title": "Account Manager", "email": "", "phone": "",
"avatar_source": "profile_picture",
"action_buttons": {
"primary": "message",
"request": { "enabled": 1, "text": "" },
"schedule": { "enabled": 1, "text": "", "calendar_link": "", "open_in_iframe": 0 },
"message": { "enabled": 1, "text": "" }
}
},
"branding": { "logo_url": "https://d1ea3bsweroqag.cloudfront.net/assets/client/logo/cl-1234.png", "custom_css": "" },
"email_notifications": {
"summary_email_frequency": "daily",
"emails": {
"invitation": { "subject": "", "body": "" },
"task_completed": { "subject": "", "button_text": "", "body": "" },
"action_required": { "subject": "", "button_text": "", "body": "" },
"file_requested": { "subject": "", "button_text": "", "body": "" },
"needs_attention": { "subject": "", "button_text": "", "body": { "main_heading": "", "task_heading": "", "notification_heading": "", "last_heading": "" } }
}
},
"variables": [{ "name": "project_manager", "scope": "portal", "value": "Jane" }]
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"portal_id\":[\"The portal id field is required.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Portal record not found."
}Create a Conversation
Create a new conversation inside an existing portal. Requires a valid portal_id.
1 to not create an email thread for this conversation. Default 0. Always treated as 1 on HIPAA-compliant portals.1 to make the conversation visible to the client. Default 0.1 to share the conversation with your internal team. Default 0.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
'https://ahsuite.com/api/portal/conversation/create?portal_id=PORTAL_ID&name=Conversation+Name'$ch = curl_init();
$data = [ 'portal_id' => 'PORTAL_ID', 'name' => 'Conversation Name' ];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/conversation/create');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const url = new URL('https://ahsuite.com/api/portal/conversation/create');
url.searchParams.set('portal_id', 'PORTAL_ID');
url.searchParams.set('name', 'Conversation Name');
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch(url, {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/conversation/create',
auth=('EMAIL', 'TOKEN'),
params={'portal_id': 'PORTAL_ID', 'name': 'Conversation Name'},
)
print(res.json()){
"success": true,
"message": "Conversation has been added successfully.",
"data": []
}{
"success": false,
"message": "Validation Error",
"data": "{\"name\":[\"The name field is required.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Portal record not found."
}List Conversations
Return the conversations of a portal, most recently updated first. Archived conversations are excluded, and visibility matches what the account owner sees in the app — conversations shared with the team plus the owner's own, while other agents' private conversations stay hidden. Results are paginated.
1.20, maximum 100.Other unsuccessful response codes: 403, 429, 500.
curl --request GET \
-u "EMAIL:TOKEN" \
'https://ahsuite.com/api/portal/conversation/list?portal_id=vJW4&page=1&limit=20'$params = http_build_query([
'portal_id' => 'vJW4',
'page' => 1,
'limit' => 20,
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/conversation/list?'.$params);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const params = new URLSearchParams({ portal_id: 'vJW4', page: 1, limit: 20 });
const res = await fetch(`https://ahsuite.com/api/portal/conversation/list?${params}`, {
headers: { Authorization: `Basic ${auth}` },
});
console.log(await res.json());import requests
res = requests.get(
'https://ahsuite.com/api/portal/conversation/list',
params={'portal_id': 'vJW4', 'page': 1, 'limit': 20},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "",
"data": {
"conversations": [
{
"name": "Website revamp discussion",
"new_message": 1,
"created_date": "2026-06-10 09:22:45",
"last_updated": "2026-06-14 16:03:12",
"conversation_id": 271
},
{
"name": "Onboarding questions",
"new_message": 0,
"created_date": "2026-05-28 11:40:03",
"last_updated": "2026-06-01 10:15:57",
"conversation_id": 265
}
],
"pagination": {
"total": 12,
"page": 1,
"limit": 20,
"total_pages": 1
}
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"portal_id\":[\"The portal id field is required.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Portal record not found."
}Conversation Detail
Return a single conversation with its last 25 messages (oldest of those first). The conversation_id comes from List Conversations. Share flags use the same names as Create Conversation.
Other unsuccessful response codes: 403, 429, 500.
curl --request GET \
-u "EMAIL:TOKEN" \
'https://ahsuite.com/api/portal/conversation/detail?portal_id=vJW4&conversation_id=271'$params = http_build_query([
'portal_id' => 'vJW4',
'conversation_id' => 271,
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/conversation/detail?'.$params);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const params = new URLSearchParams({ portal_id: 'vJW4', conversation_id: 271 });
const res = await fetch(`https://ahsuite.com/api/portal/conversation/detail?${params}`, {
headers: { Authorization: `Basic ${auth}` },
});
console.log(await res.json());import requests
res = requests.get(
'https://ahsuite.com/api/portal/conversation/detail',
params={'portal_id': 'vJW4', 'conversation_id': 271},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "",
"data": {
"name": "Website revamp discussion",
"share_with_client": 1,
"share_with_team": 1,
"email_not_send": 0,
"created_date": "2026-06-10 09:22:45",
"last_updated": "2026-06-14 16:03:12",
"logs": [
{
"message": "<p>Here is the first homepage draft.</p>",
"from": "team",
"created_date": "2026-06-14 15:58:02"
},
{
"message": "<p>Looks great, approved!</p>",
"from": "client",
"created_date": "2026-06-14 16:03:12"
}
],
"conversation_id": 271
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"conversation_id\":[\"The conversation id field is required.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Conversation record not found."
}Reply to a Conversation
Post a reply in an existing conversation — exactly like replying in the app. The team is notified and, when the conversation is shared with the client, the client is notified and emailed (unless emails are turned off for that conversation). One optional attachment per request (multipart/form-data).
404.p, br, strong, em, ul, ol, li, a; other tags are stripped.multipart/form-data). Same allowed types as the app; max 9 MB. Not available through the MCP connector.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "conversation_id=276" \
--data-urlencode "message=Thanks, reviewing this now." \
'https://ahsuite.com/api/portal/conversation/reply'$ch = curl_init();
$data = [
'portal_id' => 'vJW4',
'conversation_id' => 276,
'message' => 'Thanks, reviewing this now.',
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/conversation/reply');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams({ portal_id: 'vJW4', conversation_id: '276', message: 'Thanks, reviewing this now.' });
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/conversation/reply', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/conversation/reply',
data={'portal_id': 'vJW4', 'conversation_id': 276, 'message': 'Thanks, reviewing this now.'},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Message has been added successfully.",
"data": {
"conversation_id": 276,
"message_id": 9120
}
}{
"success": false,
"message": "Validation Error",
"data": "This Conversation is maxed out! You'll need to create a new Conversation to continue the discussion."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Conversation record not found."
}Update a Conversation
Rename a conversation and/or turn its email notifications on or off. This is a partial update: only the parameters you send are changed. On HIPAA compliant portals conversation emails are always off and cannot be turned on, as in the app.
1 stops emailing the client about this conversation (they still see it in the portal), 0 emails them again — the "email thread" toggle in the app. Returns 400 when trying to turn emails on for a HIPAA compliant portal.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "conversation_id=276" \
--data-urlencode "name=Q3 planning" \
--data-urlencode "email_not_send=0" \
'https://ahsuite.com/api/portal/conversation/update'$ch = curl_init();
$data = [
'portal_id' => 'vJW4',
'conversation_id' => 276,
'name' => 'Q3 planning',
'email_not_send' => 0,
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/conversation/update');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams({ portal_id: 'vJW4', conversation_id: '276', name: 'Q3 planning', email_not_send: '0' });
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/conversation/update', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/conversation/update',
data={'portal_id': 'vJW4', 'conversation_id': 276, 'name': 'Q3 planning', 'email_not_send': 0},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Conversation has been updated successfully.",
"data": {
"conversation_id": 276
}
}{
"success": false,
"message": "Validation Error",
"data": "This setting cannot be changed for a HIPAA compliant client."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Conversation record not found."
}Create a Page
Create a new page inside an existing portal with text, HTML code, an embedded URL, a video, an image, files (uploads or file requests) and/or form questions. The content blocks are auto-detected — any content field you send becomes visible on the page. All content fields are optional; sending only a title creates a blank page you can edit later. Returns the new page_id on success.
text[content] is sent.code[content] is sent.embed[url] is sent.video[url] is sent.multipart/form-data. Allowed types: jpg, png, jpeg, gif, svg, webp. Max 5MB.image[file] is sent.files[N] index is ONE item: either a file upload or a file request, never both. Max 10 items per page.files[0]multipart/form-data. Added to the portal's Files and shown on the page. Allowed types: doc, pdf, docx, zip, txt, xls, xlsx, csv, jpg, png, jpeg, webp, gif, html, svg, mp4, mp3, wav, mov and more. Max 15MB per file.files[1] (its own index, no file field)questions[N] index is ONE question. Max 20 questions per page.section and repeat_section — a group of questions (a repeat section can be filled in several times by the client); put its questions in questions[0][questions][M][...].paragraph (default), multiple_choice, checkbox, date, heading, file, signature.0, 1, 2…) to add multiple questions.1 to make the question required. Default 0.multiple_choice and checkbox types only. Max 20 options per question.portal (default) or user. Used only when variable_name is sent.questions[0][question_type] is section or repeat_section: the questions inside that section, with the same fields (question_type, question_name, description, required, options[], variable_name, variable_scope). One level only. They count toward the 20-question limit.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
-F 'portal_id=PORTAL_ID' \
-F 'title=Page Title' \
-F 'text[content]=Welcome to your new page.' \
-F 'text[heading]=Getting Started' \
-F 'code[content]=<div><h3>Hello</h3></div>' \
-F 'embed[url]=https://docs.google.com/document/d/XXXX' \
-F 'video[url]=https://www.youtube.com/watch?v=XXXX' \
-F 'image[file]=@/path/to/photo.jpg' \
-F 'files[0][file]=@/path/to/brief.pdf' \
-F 'files[0][file_name]=Project Brief' \
-F 'files[1][title]=Signed contract' \
-F 'questions[0][question_name]=What is your main business goal?' \
-F 'questions[1][question_type]=multiple_choice' \
-F 'questions[1][question_name]=Preferred contact method?' \
-F 'questions[1][options][]=Email' \
-F 'questions[1][options][]=Phone' \
'https://ahsuite.com/api/portal/page/create'$ch = curl_init();
$data = [
'portal_id' => 'PORTAL_ID',
'title' => 'Page Title',
'text[content]' => 'Welcome to your new page.',
'text[heading]' => 'Getting Started',
'code[content]' => '<div><h3>Hello</h3></div>',
'embed[url]' => 'https://docs.google.com/document/d/XXXX',
'video[url]' => 'https://www.youtube.com/watch?v=XXXX',
'image[file]' => new CURLFile('/path/to/photo.jpg'),
'files[0][file]' => new CURLFile('/path/to/brief.pdf'),
'files[0][file_name]' => 'Project Brief',
'files[1][title]' => 'Signed contract',
'questions[0][question_name]' => 'What is your main business goal?',
'questions[1][question_type]' => 'multiple_choice',
'questions[1][question_name]' => 'Preferred contact method?',
'questions[1][options][0]' => 'Email',
'questions[1][options][1]' => 'Phone',
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/page/create');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data); // array = multipart/form-data (required for image[file])
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
import FormData from 'form-data';
import fs from 'fs';
const form = new FormData();
form.append('portal_id', 'PORTAL_ID');
form.append('title', 'Page Title');
form.append('text[content]', 'Welcome to your new page.');
form.append('text[heading]', 'Getting Started');
form.append('code[content]', '<div><h3>Hello</h3></div>');
form.append('embed[url]', 'https://docs.google.com/document/d/XXXX');
form.append('video[url]', 'https://www.youtube.com/watch?v=XXXX');
form.append('image[file]', fs.createReadStream('/path/to/photo.jpg'));
form.append('files[0][file]', fs.createReadStream('/path/to/brief.pdf'));
form.append('files[0][file_name]', 'Project Brief');
form.append('files[1][title]', 'Signed contract');
form.append('questions[0][question_name]', 'What is your main business goal?');
form.append('questions[1][question_type]', 'multiple_choice');
form.append('questions[1][question_name]', 'Preferred contact method?');
form.append('questions[1][options][]', 'Email');
form.append('questions[1][options][]', 'Phone');
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/page/create', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body: form,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/page/create',
auth=('EMAIL', 'TOKEN'),
data={
'portal_id': 'PORTAL_ID',
'title': 'Page Title',
'text[content]': 'Welcome to your new page.',
'text[heading]': 'Getting Started',
'code[content]': '<div><h3>Hello</h3></div>',
'embed[url]': 'https://docs.google.com/document/d/XXXX',
'video[url]': 'https://www.youtube.com/watch?v=XXXX',
'files[0][file_name]': 'Project Brief',
'files[1][title]': 'Signed contract',
'questions[0][question_name]': 'What is your main business goal?',
'questions[1][question_type]': 'multiple_choice',
'questions[1][question_name]': 'Preferred contact method?',
'questions[1][options][0]': 'Email',
'questions[1][options][1]': 'Phone',
},
files={
'image[file]': open('/path/to/photo.jpg', 'rb'),
'files[0][file]': open('/path/to/brief.pdf', 'rb'),
},
)
print(res.json()){
"success": true,
"message": "Page has been added successfully.",
"data": {
"page_id": "vJW4"
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"title\":[\"The title field is required.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Portal record not found."
}List Pages
Return the pages of a portal, in the same order they appear in the app (grouped by category, then by manual order). Archived pages are excluded. Results are paginated.
1.20, maximum 100.Other unsuccessful response codes: 403, 429, 500.
curl --request GET \
-u "EMAIL:TOKEN" \
'https://ahsuite.com/api/portal/page/list?portal_id=vJW4&page=1&limit=20'$params = http_build_query([
'portal_id' => 'vJW4',
'page' => 1,
'limit' => 20,
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/page/list?'.$params);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const params = new URLSearchParams({ portal_id: 'vJW4', page: 1, limit: 20 });
const res = await fetch(`https://ahsuite.com/api/portal/page/list?${params}`, {
headers: { Authorization: `Basic ${auth}` },
});
console.log(await res.json());import requests
res = requests.get(
'https://ahsuite.com/api/portal/page/list',
params={'portal_id': 'vJW4', 'page': 1, 'limit': 20},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "",
"data": {
"pages": [
{
"title": "Monthly SEO Report",
"category": "Reports",
"draft": 0,
"created_date": "2026-06-02 14:20:11",
"page_id": "9dRt"
},
{
"title": "Onboarding Checklist",
"category": "",
"draft": 1,
"created_date": "2026-05-18 09:31:47",
"page_id": "8kLm"
}
],
"pagination": {
"total": 8,
"page": 1,
"limit": 20,
"total_pages": 1
}
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"portal_id\":[\"The portal id field is required.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Portal record not found."
}Page Detail
Return a single page with its content — title, category, draft flag and its tabs in the order the page editor shows them. Each tab returns its text block (HTML content) embed block (embedded URL or embed code, plus height) image block (file_name plus a temporary url valid for 20 minutes) and files block (items[] — the files and file requests placed on the tab, with the same file / file_name / title / description fields Create a Page accepts (file = the uploaded file, file_name = its display name; title and description belong to file requests), type (file or url), a url (the link itself for url items, a temporary download link valid for 20 minutes for uploaded files, empty for an open file request), is_request = 1 for a file request, and a file_id that matches List Files) video block (YouTube url), code block (raw HTML/CSS/JS content) and questions block (items[] — the form questions, with the same fields Create a Page accepts plus a question_id; section and repeat_section containers carry their child questions in a nested questions[]), each with the optional block heading and description, using the same field names Create a Page accepts; a block the tab does not have is returned as null. Archived pages are not returned. Get a page_id from List Pages.
Other unsuccessful response codes: 403, 429, 500.
curl --request GET \
-u "EMAIL:TOKEN" \
'https://ahsuite.com/api/portal/page/detail?portal_id=vJW4&page_id=9dRt'$params = http_build_query([
'portal_id' => 'vJW4',
'page_id' => '9dRt',
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/page/detail?'.$params);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const params = new URLSearchParams({ portal_id: 'vJW4', page_id: '9dRt' });
const res = await fetch(`https://ahsuite.com/api/portal/page/detail?${params}`, {
headers: { Authorization: `Basic ${auth}` },
});
console.log(await res.json());import requests
res = requests.get(
'https://ahsuite.com/api/portal/page/detail',
params={'portal_id': 'vJW4', 'page_id': '9dRt'},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "",
"data": {
"title": "Monthly SEO Report",
"category": "Reports",
"draft": 0,
"created_date": "2026-06-02 14:20:11",
"page_id": "9dRt",
"tabs": [
{
"tab_id": "3xQa",
"title": "Tab 1",
"text": {
"content": "<h2>Summary</h2><p>Organic traffic grew 18% this month.</p>",
"heading": "June overview",
"description": ""
},
"embed": null,
"image": {
"file_name": "traffic-chart.png",
"url": "https://s3.amazonaws.com/.../view/API-1718000000-traffic-chart.png?X-Amz-Expires=1200&...",
"heading": "",
"description": ""
},
"files": null,
"video": null,
"code": {
"content": "<div class=\"cta\"><a href=\"https://example.com/book\">Book a call</a></div>\n<style>.cta a{background:#2f80ed;color:#fff;padding:10px 16px;border-radius:6px}</style>",
"heading": "",
"description": ""
},
"questions": null
},
{
"tab_id": "3xQb",
"title": "Tab 2",
"text": null,
"embed": {
"url": "https://docs.google.com/spreadsheets/d/1AbC/edit",
"height": 800,
"heading": "Live report",
"description": "Updated every Monday."
},
"image": null,
"files": {
"heading": "Documents",
"description": "",
"items": [
{ "type": "file", "file": "Contract.pdf", "url": "https://s3.amazonaws.com/.../tool/API-1718000000-Contract.pdf?X-Amz-Expires=1200&...", "file_name": "Signed contract", "title": "", "description": "", "is_request": 0, "file_id": "7pQ1" },
{ "type": "file", "file": "", "url": "", "file_name": "", "title": "Please upload your logo", "description": "PNG or SVG, transparent background.", "is_request": 1, "file_id": "7pQ2" }
]
},
"video": {
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"heading": "Walkthrough",
"description": ""
},
"code": null,
"questions": {
"heading": "Kick-off form",
"description": "Takes 2 minutes.",
"items": [
{ "question_type": "paragraph", "question_name": "What is your main goal this quarter?", "description": "", "required": 1, "options": [], "variable_name": "main_goal", "variable_scope": "portal", "question_id": "5wA1" },
{ "question_type": "multiple_choice", "question_name": "Preferred contact method", "description": "", "required": 0, "options": ["Email", "Phone", "Slack"], "variable_name": "", "variable_scope": "", "question_id": "5wA2" },
{ "question_type": "repeat_section", "question_name": "Team members", "description": "", "required": 0, "options": [], "variable_name": "", "variable_scope": "", "question_id": "5wA3",
"questions": [
{ "question_type": "paragraph", "question_name": "Full name", "description": "", "required": 1, "options": [], "variable_name": "", "variable_scope": "", "question_id": "5wA4" }
]
}
]
}
}
]
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"page_id\":[\"The page id field is required.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Page record not found."
}Update a Page
Update an existing page. This is a partial update: only the parameters you send are changed, every other field and block on the page is left untouched. Sending a parameter as an empty string clears that field. Page-level fields (title, draft, category) need only the page_id; content blocks are edited per tab and need the tab_id from Page Detail. All content blocks are supported: text, code, video, image, files, questions and embed. Send the request as multipart/form-data when uploading an image or files.
1 marks the page as a draft (hidden from the client), 0 publishes it.tabs[].tab_id. Required when any block parameter or tab_title is sent. New tabs are created with Add a Tab.tab_id. Max 100 characters.0 hides the text block on this tab, 1 shows it again. Content is kept either way (the X Text / + Text toggle). Sending any other text field turns the block on automatically.0 hides the code block on this tab, 1 shows it again. Content is kept either way. Sending any other code field turns the block on automatically.0 hides the video block on this tab, 1 shows it again. Content is kept either way. Sending any other video field turns the block on automatically.0 hides the image block on this tab, 1 shows it again. The image is kept either way. Sending any other image field turns the block on automatically.multipart/form-data). Allowed types: jpg, jpeg, png, gif, svg, webp. Max 5 MB. Replaces the current image on the block. Not available through the MCP connector.0 hides the files block on this tab, 1 shows it again. The files are kept either way. Sending any other files field turns the block on automatically.multipart/form-data). Same types and 15 MB limit as Create a Page. Existing files on the tab are kept; the portal-wide file limit applies to new files. Not available through the MCP connector.file_id of a file already in this portal (from List Files, uploads and links alike) to place on the tab. A file already on the tab is skipped. Files of other portals return 404.title without a file to add a file request — a card asking the client to upload something. Max 100 characters.0 hides the form block on this tab, 1 shows it again. The questions are kept either way. Sending any other questions field turns the block on automatically.questions[N][...] items as Create a Page: paragraph (default), multiple_choice, checkbox, date, heading, file, signature, plus section and repeat_section (a group of questions; a repeat section can be filled in several times by the client). Every call appends — existing questions are never changed or removed. A question whose name already exists on the tab (case-insensitive) is skipped and returned in skipped_questions. Max 20 per request, 250 per tab.1 makes the question mandatory. Default 0.multiple_choice or checkbox question, one per options[] entry. Required for those types. Max 20, each max 500 characters.portal (default) or user — scope of variable_name.section or repeat_section of this tab — its question_id from Page Detail. Not allowed on section types themselves. A section of another tab or portal returns 404.questions[0][question_type] is section or repeat_section: the questions to create inside the new section, with the same fields (question_type, question_name, description, required, options[], variable_name, variable_scope). One level only — a section cannot contain another section. Max 20.0 hides the embed block on this tab, 1 shows it again. The block's content is kept either way — the same as the X Embed / + Embed toggle in the page editor. Sending any other embed field turns the block on automatically.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "page_id=9dRt" \
--data-urlencode "title=Monthly SEO Report (June)" \
--data-urlencode "tab_id=3xQb" \
--data-urlencode "embed[url]=https://docs.google.com/spreadsheets/d/1AbC/edit" \
--data-urlencode "embed[height]=800" \
'https://ahsuite.com/api/portal/page/update'$ch = curl_init();
$data = [
'portal_id' => 'vJW4',
'page_id' => '9dRt',
'title' => 'Monthly SEO Report (June)',
'tab_id' => '3xQb',
'embed' => ['url' => 'https://docs.google.com/spreadsheets/d/1AbC/edit', 'height' => 800],
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/page/update');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams({
portal_id: 'vJW4',
page_id: '9dRt',
title: 'Monthly SEO Report (June)',
tab_id: '3xQb',
'embed[url]': 'https://docs.google.com/spreadsheets/d/1AbC/edit',
'embed[height]': '800',
});
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/page/update', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/page/update',
data={
'portal_id': 'vJW4',
'page_id': '9dRt',
'title': 'Monthly SEO Report (June)',
'tab_id': '3xQb',
'embed[url]': 'https://docs.google.com/spreadsheets/d/1AbC/edit',
'embed[height]': 800,
},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Page has been updated successfully.",
"data": {
"page_id": "9dRt",
"skipped_questions": ["Preferred contact"]
}
}{
"success": false,
"message": "Validation Error",
"data": "tab_id is required when sending content blocks (get it from page/detail)."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Tab record not found."
}Add a Tab
Add a new, empty tab to a page — the same as + Add Tab in the page editor. Returns the new tab_id; fill it with content blocks using Update a Page. A page can have at most 20 tabs.
Tab N. Can be changed later with tab_title on Update a Page.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "page_id=9dRt" \
--data-urlencode "title=Resources" \
'https://ahsuite.com/api/portal/page/tab/add'$ch = curl_init();
$data = [
'portal_id' => 'vJW4',
'page_id' => '9dRt',
'title' => 'Resources',
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/page/tab/add');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams({ portal_id: 'vJW4', page_id: '9dRt', title: 'Resources' });
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/page/tab/add', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/page/tab/add',
data={'portal_id': 'vJW4', 'page_id': '9dRt', 'title': 'Resources'},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Tab has been added successfully.",
"data": {
"tab_id": "3xQc",
"title": "Resources",
"page_id": "9dRt"
}
}{
"success": false,
"message": "Validation Error",
"data": "Maximum 20 tabs can be added."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Page record not found."
}Update a Question
Edit one form question of a page — the same as the pencil icon in the page editor. This is a partial update: only the parameters you send are changed. As in the app, only the question text, its description and the required flag can be edited; the type, options and variable are fixed once a question is created (add a new question with Update a Page and delete the old one instead). Get the question_id from Page Detail.
question_id from Page Detail. A question of another page or portal returns 404.1 makes the question mandatory, 0 optional.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "page_id=9dRt" \
--data-urlencode "question_id=5wA2" \
--data-urlencode "question_name=Preferred contact method" \
--data-urlencode "required=1" \
'https://ahsuite.com/api/portal/page/question/update'$ch = curl_init();
$data = [
'portal_id' => 'vJW4',
'page_id' => '9dRt',
'question_id' => '5wA2',
'question_name' => 'Preferred contact method',
'required' => 1,
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/page/question/update');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams({
portal_id: 'vJW4',
page_id: '9dRt',
question_id: '5wA2',
question_name: 'Preferred contact method',
required: '1',
});
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/page/question/update', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/page/question/update',
data={'portal_id': 'vJW4', 'page_id': '9dRt', 'question_id': '5wA2', 'question_name': 'Preferred contact method', 'required': 1},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Question has been updated successfully.",
"data": {
"question_id": "5wA2",
"page_id": "9dRt"
}
}{
"success": false,
"message": "Validation Error",
"data": "No settings were sent to update."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Question record not found on this page."
}Move a Question
Move an existing question into a section or repeat_section on the same tab — the same as Add Question inside a section in the page editor — or move it back out to the top level (Move Out). Sections themselves and questions with conditional navigation cannot be placed inside a section. To create a new question directly inside a section, use Update a Page with questions[0][section_id].
question_id of the target section or repeat_section; it must be on the same tab as the question. Send an empty string to move the question out of its section.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "page_id=9dRt" \
--data-urlencode "question_id=5wA2" \
--data-urlencode "section_id=5wA3" \
'https://ahsuite.com/api/portal/page/question/move'$ch = curl_init();
$data = [
'portal_id' => 'vJW4',
'page_id' => '9dRt',
'question_id' => '5wA2',
'section_id' => '5wA3',
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/page/question/move');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams({ portal_id: 'vJW4', page_id: '9dRt', question_id: '5wA2', section_id: '5wA3' });
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/page/question/move', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/page/question/move',
data={'portal_id': 'vJW4', 'page_id': '9dRt', 'question_id': '5wA2', 'section_id': '5wA3'},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Question has been moved into the section successfully.",
"data": {
"question_id": "5wA2",
"page_id": "9dRt",
"section_id": "5wA3"
}
}{
"success": false,
"message": "Validation Error",
"data": "A section cannot be moved inside another section."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Section record not found on the same tab as the question."
}Delete a Question
Remove one form question from a page — the same as the trash icon in the page editor. Deleting a section or repeat_section also removes the questions inside it. Answers the client already gave stay available in the form responses. This cannot be undone. Get the question_id from Page Detail.
question_id from Page Detail. A question of another page or portal returns 404.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "page_id=9dRt" \
--data-urlencode "question_id=5wA2" \
'https://ahsuite.com/api/portal/page/question/delete'$ch = curl_init();
$data = [
'portal_id' => 'vJW4',
'page_id' => '9dRt',
'question_id' => '5wA2',
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/page/question/delete');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams({ portal_id: 'vJW4', page_id: '9dRt', question_id: '5wA2' });
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/page/question/delete', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/page/question/delete',
data={'portal_id': 'vJW4', 'page_id': '9dRt', 'question_id': '5wA2'},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Question has been deleted successfully.",
"data": {
"question_id": "5wA2",
"page_id": "9dRt"
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"question_id\":[\"The question id field is required.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Question record not found on this page."
}Create a Task
Create a new task inside an existing portal. By default the task is private and assigned to you; send assign_to_client=1 to make it visible to the client user (client users with email notifications enabled are notified). Returns the new task_id on success.
p, br, strong, em, ul, ol, li, a). Max 5000 characters. Optional.YYYY-MM-DD format. Optional.0 (default) = private task assigned to you. 1 = client task, visible to the client user — client users who have email notifications enabled are notified by email.0, 1, 2…) to add multiple labels. An existing label with the same name and color is reused. Optional.green, yellow, orange, red, purple, blue, sky, lime, pink, black. Optional, default = no color.multipart/form-data — send the parameter multiple times to attach multiple files. Allowed types: doc, pdf, docx, zip, txt, xls, xlsx, csv, jpg, png, jpeg, webp, gif, html, svg, mp4, mp3, wav, mov and more. Max 9MB per file, max 10 files per request. Optional.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
-F 'portal_id=PORTAL_ID' \
-F 'name=Follow up with the client' \
-F 'description=Call to review the latest deliverables.' \
-F 'due_date=2026-08-01' \
-F 'assign_to_client=0' \
-F 'checklist[]=Draft the report' \
-F 'checklist[]=Send for review' \
-F 'labels[0][name]=Priority' \
-F 'labels[0][color]=red' \
-F 'attachments[]=@/path/to/spec.pdf' \
'https://ahsuite.com/api/portal/task/create'$ch = curl_init();
$data = [
'portal_id' => 'PORTAL_ID',
'name' => 'Follow up with the client',
'description' => 'Call to review the latest deliverables.',
'due_date' => '2026-08-01',
'assign_to_client' => 0,
'checklist[0]' => 'Draft the report',
'checklist[1]' => 'Send for review',
'labels[0][name]' => 'Priority',
'labels[0][color]' => 'red',
'attachments[0]' => new CURLFile('/path/to/spec.pdf'),
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/task/create');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data); // array = multipart/form-data (required for attachments[])
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
import FormData from 'form-data';
import fs from 'fs';
const form = new FormData();
form.append('portal_id', 'PORTAL_ID');
form.append('name', 'Follow up with the client');
form.append('description', 'Call to review the latest deliverables.');
form.append('due_date', '2026-08-01');
form.append('assign_to_client', '0');
form.append('checklist[]', 'Draft the report');
form.append('checklist[]', 'Send for review');
form.append('labels[0][name]', 'Priority');
form.append('labels[0][color]', 'red');
form.append('attachments[]', fs.createReadStream('/path/to/spec.pdf'));
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/task/create', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body: form,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/task/create',
auth=('EMAIL', 'TOKEN'),
data={
'portal_id': 'PORTAL_ID',
'name': 'Follow up with the client',
'description': 'Call to review the latest deliverables.',
'due_date': '2026-08-01',
'assign_to_client': 0,
'checklist[]': ['Draft the report', 'Send for review'],
'labels[0][name]': 'Priority',
'labels[0][color]': 'red',
},
files={'attachments[]': open('/path/to/spec.pdf', 'rb')},
)
print(res.json()){
"success": true,
"message": "Task has been added successfully.",
"data": {
"task_id": "vJW4"
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"name\":[\"The name field is required.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Portal record not found."
}List Tasks
Return the tasks of a portal, in the same order the app shows them (tasks being worked on first). By default every non-cancelled, non-archived task is returned — use status to filter. Results are paginated.
ready_for_work, pending, in_progress, on_hold, done, canceled. When omitted, every status except canceled is returned.1.20, maximum 100.Other unsuccessful response codes: 403, 429, 500.
curl --request GET \
-u "EMAIL:TOKEN" \
'https://ahsuite.com/api/portal/task/list?portal_id=vJW4&status=in_progress&page=1&limit=20'$params = http_build_query([
'portal_id' => 'vJW4',
'status' => 'in_progress',
'page' => 1,
'limit' => 20,
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/task/list?'.$params);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const params = new URLSearchParams({ portal_id: 'vJW4', status: 'in_progress', page: 1, limit: 20 });
const res = await fetch(`https://ahsuite.com/api/portal/task/list?${params}`, {
headers: { Authorization: `Basic ${auth}` },
});
console.log(await res.json());import requests
res = requests.get(
'https://ahsuite.com/api/portal/task/list',
params={'portal_id': 'vJW4', 'status': 'in_progress', 'page': 1, 'limit': 20},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "",
"data": {
"tasks": [
{
"name": "Design homepage mockup",
"status": "in_progress",
"assign_to_client": 0,
"assigned_to": "John Smith",
"due_date": "2026-06-20 00:00:00",
"created_date": "2026-06-05 10:12:30",
"task_id": 512
},
{
"name": "Send onboarding documents",
"status": "pending",
"assign_to_client": 1,
"assigned_to": "Client",
"due_date": "2026-06-25 00:00:00",
"created_date": "2026-06-08 13:45:19",
"task_id": 518
}
],
"pagination": {
"total": 14,
"page": 1,
"limit": 20,
"total_pages": 1
}
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"status\":[\"The selected status is invalid.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Portal record not found."
}Task Detail
Return a single task in full — name, description, status, dates, assignee, project, checklist, labels, attachments and comments, as shown on the task detail screen in the app. Field names match Create Task. Hidden and archived tasks are not returned. Get a task_id from List Tasks.
404.Other unsuccessful response codes: 403, 429, 500.
curl --request GET \
-u "EMAIL:TOKEN" \
'https://ahsuite.com/api/portal/task/detail?portal_id=vJW4&task_id=3659'$params = http_build_query([
'portal_id' => 'vJW4',
'task_id' => 3659,
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/task/detail?'.$params);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const params = new URLSearchParams({ portal_id: 'vJW4', task_id: '3659' });
const res = await fetch(`https://ahsuite.com/api/portal/task/detail?${params}`, {
headers: { Authorization: `Basic ${auth}` },
});
console.log(await res.json());import requests
res = requests.get(
'https://ahsuite.com/api/portal/task/detail',
params={'portal_id': 'vJW4', 'task_id': 3659},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "",
"data": {
"name": "Send the June report",
"description": "<p>Export the analytics and share the PDF.</p>",
"status": "in_progress",
"due_date": "2026-06-30 00:00:00",
"assign_to_client": 0,
"assigned_to": "Jane Cooper",
"project": "Q2 Reporting",
"created_date": "2026-06-02 14:20:11",
"task_id": 3659,
"checklist": [
{ "name": "Export analytics", "completed": 1, "item_id": 8101 },
{ "name": "Write summary", "completed": 0, "item_id": 8102 }
],
"labels": [
{ "name": "Reporting", "color": "blue" }
],
"attachments": [
{ "name": "may-report.pdf", "url": "https://s3.amazonaws.com/.../task/T-1718000000-may-report.pdf?X-Amz-Expires=900&...", "attachment_id": 512 }
],
"comments": [
{ "comment": "Draft is ready for review.", "by": "Jane Cooper", "from": "agent", "created_date": "2026-06-10 09:15:02", "comment_id": 2201 },
{ "comment": "Please include the ads data too.", "by": "Sam Lee (Client)", "from": "client", "created_date": "2026-06-09 17:40:11", "comment_id": 2198 }
]
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"task_id\":[\"The task id field is required.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Task record not found."
}Update a Task
Update an existing task. This is a partial update: only the parameters you send are changed, everything else on the task is left untouched. Each change is written to the task's history, as in the app. Supported: name, description, due_date, status, dependency_task_id and project_id. Assignee, labels, checklist items, attachments and comments have their own endpoints below. Get a task_id from List Tasks.
p, br, strong, em, ul, ol, li, a), other tags are stripped. Max 5000 characters. Replaces the current description. Empty string clears it.YYYY-MM-DD format. Empty string removes the due date.ready_for_work, pending, in_progress, on_hold, done or canceled — the same values List Tasks returns. Behaves exactly like changing the status in the app: marking a task done notifies the team (and emails the client when they created the task), and tasks that were waiting on this one become ready for work.task_id of another open task in this portal that must be finished first (the "Dependency" field). The task is put on pending until that task is done or canceled, then becomes ready for work automatically, as in the app. Send 0 to remove the dependency. A done, canceled or unknown task returns 404.project_id from List Projects to place the task in. Send 0 to take the task out of its project. A project of another portal returns 404.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "task_id=3659" \
--data-urlencode "name=Send the June report" \
--data-urlencode "due_date=2026-06-30" \
'https://ahsuite.com/api/portal/task/update'$ch = curl_init();
$data = [
'portal_id' => 'vJW4',
'task_id' => 3659,
'name' => 'Send the June report',
'due_date' => '2026-06-30',
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/task/update');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams({
portal_id: 'vJW4',
task_id: '3659',
name: 'Send the June report',
due_date: '2026-06-30',
});
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/task/update', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/task/update',
data={'portal_id': 'vJW4', 'task_id': 3659, 'name': 'Send the June report', 'due_date': '2026-06-30'},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Task has been updated successfully.",
"data": {
"task_id": 3659
}
}{
"success": false,
"message": "Validation Error",
"data": "No settings were sent to update."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Task record not found."
}Assign a Task
Change who a task is assigned to — a team member, the client (any client user) or one specific client user. Behaves exactly like the assignee dropdown in the app: assigning to the client or a client user shares the task with them and sends the "action required" email; assigning to a team member notifies them; the change is written to the task history. A task created by the client always stays shared with all client users: you can add a team member to it (team_member), but client and client_user return 400 for such a task, as in the app.
team_member — a member of the portal's team (send their email); client — any client user of the portal; client_user — one specific client user (send their email).team_member and client_user: the person's email address. Team members must be assigned to this portal; client users must be active in it (see List Contacts). Otherwise 404.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "task_id=3659" \
--data-urlencode "assign_to=client_user" \
--data-urlencode "email=jane@example.com" \
'https://ahsuite.com/api/portal/task/assign'$ch = curl_init();
$data = [
'portal_id' => 'vJW4',
'task_id' => 3659,
'assign_to' => 'client_user',
'email' => 'jane@example.com',
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/task/assign');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams({ portal_id: 'vJW4', task_id: '3659', assign_to: 'client_user', email: 'jane@example.com' });
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/task/assign', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/task/assign',
data={'portal_id': 'vJW4', 'task_id': 3659, 'assign_to': 'client_user', 'email': 'jane@example.com'},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Task has been assigned successfully.",
"data": {
"task_id": 3659,
"assigned_to": "Jane"
}
}{
"success": false,
"message": "Validation Error",
"data": "Not allowed to remove client from assignee"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "No active client user with this email in this portal."
}Add Task Labels
Add one or more labels to a task — the same as Add Label on the task screen. A label with the same name and color already in the portal is reused, otherwise it is created. Labels already on the task are kept.
green, yellow, orange, red, purple, blue, sky, lime, pink or black. Omit for no color.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "task_id=3659" \
--data-urlencode "labels[0][name]=Reporting" \
--data-urlencode "labels[0][color]=blue" \
--data-urlencode "labels[1][name]=Urgent" \
--data-urlencode "labels[1][color]=red" \
'https://ahsuite.com/api/portal/task/label/add'$ch = curl_init();
$data = [
'portal_id' => 'vJW4',
'task_id' => 3659,
'labels' => [
['name' => 'Reporting', 'color' => 'blue'],
['name' => 'Urgent', 'color' => 'red'],
],
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/task/label/add');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams({
portal_id: 'vJW4',
task_id: '3659',
'labels[0][name]': 'Reporting',
'labels[0][color]': 'blue',
'labels[1][name]': 'Urgent',
'labels[1][color]': 'red',
});
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/task/label/add', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/task/label/add',
data={
'portal_id': 'vJW4',
'task_id': 3659,
'labels[0][name]': 'Reporting', 'labels[0][color]': 'blue',
'labels[1][name]': 'Urgent', 'labels[1][color]': 'red',
},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Labels have been added successfully.",
"data": {
"task_id": 3659
}
}{
"success": false,
"message": "Validation Error",
"data": "These labels are already on the task — nothing to change."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Task record not found."
}Add Checklist Items
Add one or more items to a task's checklist — the same as Add item on the task screen. Existing items are kept; each addition is written to the task history. Returns the new items with their item_id.
checklist[] entry, same as Create Task. Each max 2000 characters, up to 20 per request. Blank entries are skipped.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "task_id=3659" \
--data-urlencode "checklist[]=Export analytics" \
--data-urlencode "checklist[]=Write summary" \
'https://ahsuite.com/api/portal/task/checklist/add'$ch = curl_init();
$data = [
'portal_id' => 'vJW4',
'task_id' => 3659,
'checklist' => ['Export analytics', 'Write summary'],
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/task/checklist/add');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams();
body.append('portal_id', 'vJW4');
body.append('task_id', '3659');
body.append('checklist[]', 'Export analytics');
body.append('checklist[]', 'Write summary');
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/task/checklist/add', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/task/checklist/add',
data={'portal_id': 'vJW4', 'task_id': 3659, 'checklist[]': ['Export analytics', 'Write summary']},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Checklist items have been added successfully.",
"data": {
"task_id": 3659,
"checklist": [
{ "name": "Export analytics", "completed": 0, "item_id": 8101 },
{ "name": "Write summary", "completed": 0, "item_id": 8102 }
]
}
}{
"success": false,
"message": "Validation Error",
"data": "Item name is blank."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Task record not found."
}Update a Checklist Item
Rename a checklist item and/or mark it completed or not completed — the same as editing or ticking an item on the task screen. This is a partial update: only the parameters you send are changed. Completing or un-checking an item is written to the task history.
checklist[].item_id or Add Checklist Items. An item of another task or portal returns 404.1 marks the item completed, 0 marks it not completed.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "task_id=3659" \
--data-urlencode "item_id=8101" \
--data-urlencode "completed=1" \
'https://ahsuite.com/api/portal/task/checklist/update'$ch = curl_init();
$data = [
'portal_id' => 'vJW4',
'task_id' => 3659,
'item_id' => 8101,
'completed' => 1,
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/task/checklist/update');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams({ portal_id: 'vJW4', task_id: '3659', item_id: '8101', completed: '1' });
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/task/checklist/update', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/task/checklist/update',
data={'portal_id': 'vJW4', 'task_id': 3659, 'item_id': 8101, 'completed': 1},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Checklist item has been updated successfully.",
"data": {
"task_id": 3659,
"item_id": 8101,
"name": "Export analytics",
"completed": 1
}
}{
"success": false,
"message": "Validation Error",
"data": "No settings were sent to update."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Checklist item not found on this task."
}Add a Task Attachment
Upload one file to a task — the same as Add attachment on the task screen. Send the request as multipart/form-data, one file per request. Existing attachments are kept; the upload is written to the task history. A task can hold at most 70 attachments. Not available through the MCP connector (binary upload).
Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
-F "portal_id=vJW4" \
-F "task_id=3659" \
-F "attachment=@/path/to/brief.pdf" \
'https://ahsuite.com/api/portal/task/attachment/add'$ch = curl_init();
$data = [
'portal_id' => 'vJW4',
'task_id' => 3659,
'attachment' => new CURLFile('/path/to/brief.pdf'),
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/task/attachment/add');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
import { FormData, File } from 'node-fetch';
import { readFileSync } from 'fs';
const form = new FormData();
form.append('portal_id', 'vJW4');
form.append('task_id', '3659');
form.append('attachment', new File([readFileSync('/path/to/brief.pdf')], 'brief.pdf'));
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/task/attachment/add', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body: form,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/task/attachment/add',
data={'portal_id': 'vJW4', 'task_id': 3659},
files={'attachment': open('/path/to/brief.pdf', 'rb')},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Attachment has been added successfully.",
"data": {
"task_id": 3659,
"name": "brief.pdf",
"attachment_id": 512
}
}{
"success": false,
"message": "Validation Error",
"data": "Total task attachments adding limit is 70."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Task record not found."
}Add a Task Comment
Post a comment on a task — exactly like commenting on the task screen: the comment is written to the task history and the people involved are notified (client users are notified when the task is shared with them). Basic HTML is allowed.
p, br, strong, em, ul, ol, li, a; other tags are stripped.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "task_id=3659" \
--data-urlencode "comment=Draft is ready for review." \
'https://ahsuite.com/api/portal/task/comment/add'$ch = curl_init();
$data = [
'portal_id' => 'vJW4',
'task_id' => 3659,
'comment' => 'Draft is ready for review.',
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/task/comment/add');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams({ portal_id: 'vJW4', task_id: '3659', comment: 'Draft is ready for review.' });
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/task/comment/add', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/task/comment/add',
data={'portal_id': 'vJW4', 'task_id': 3659, 'comment': 'Draft is ready for review.'},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Comment has been added successfully.",
"data": {
"task_id": 3659,
"comment_id": 2201
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"comment\":[\"The comment field is required.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Task record not found."
}Create a File
Add an item to the portal's Files section — upload a file, add a link, request a file from the client user, or create a folder. Send exactly ONE item type per request. Returns the new file_id on success (for folders, pass it as folder_id in later requests to add files inside).
1 to make the file visible to the client user. Default 0 (internal only).1 to share the file with your internal team. Default 0 (only you).file_id — places the item inside that folder. Not allowed when creating a folder (folders are root-level only). Optional.multipart/form-data. Allowed types: doc, pdf, docx, zip, txt, xls, xlsx, csv, jpg, png, jpeg, webp, gif, html, svg, mp4, mp3, wav, mov and more. Max 50MB.1 = the client user is asked to approve the file. Default 0.file_id can be used as folder_id to add files inside it.Other unsuccessful response codes: 403, 429, 500.
# upload a file (send ONE item type per request)
curl --request POST \
-u "EMAIL:TOKEN" \
-F 'portal_id=PORTAL_ID' \
-F 'upload[file]=@/path/to/contract.pdf' \
-F 'upload[require_client_approval]=1' \
'https://ahsuite.com/api/portal/file/create'
# add a link
curl --request POST \
-u "EMAIL:TOKEN" \
-F 'portal_id=PORTAL_ID' \
-F 'link[title]=Brand guidelines' \
-F 'link[url]=https://docs.google.com/document/d/XXXX' \
'https://ahsuite.com/api/portal/file/create'$ch = curl_init();
$data = [
'portal_id' => 'PORTAL_ID',
'upload[file]' => new CURLFile('/path/to/contract.pdf'),
'upload[require_client_approval]' => 1,
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/file/create');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data); // array = multipart/form-data (required for upload[file])
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
import FormData from 'form-data';
import fs from 'fs';
const form = new FormData();
form.append('portal_id', 'PORTAL_ID');
form.append('upload[file]', fs.createReadStream('/path/to/contract.pdf'));
form.append('upload[require_client_approval]', '1');
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/file/create', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body: form,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/file/create',
auth=('EMAIL', 'TOKEN'),
data={
'portal_id': 'PORTAL_ID',
'upload[require_client_approval]': 1,
},
files={'upload[file]': open('/path/to/contract.pdf', 'rb')},
)
print(res.json()){
"success": true,
"message": "File has been added successfully.",
"data": {
"file_id": "vJW4"
}
}{
"success": false,
"message": "Validation Error",
"data": "Send exactly one of: file, link[url], request[title] or folder[title]."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Portal record not found."
}List Files
Return the files of a portal — uploads, links, file requests and folders — in the same order the app shows them (folders first). By default the root level is returned; pass folder_id to list inside a folder. Archived files are excluded, and visibility matches what the account owner sees in the app. Results are paginated.
file_id of a row with type folder. When omitted, the root level is returned.1.20, maximum 100.Other unsuccessful response codes: 403, 429, 500.
curl --request GET \
-u "EMAIL:TOKEN" \
'https://ahsuite.com/api/portal/file/list?portal_id=vJW4&page=1&limit=20'$params = http_build_query([
'portal_id' => 'vJW4',
'page' => 1,
'limit' => 20,
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/file/list?'.$params);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const params = new URLSearchParams({ portal_id: 'vJW4', page: 1, limit: 20 });
const res = await fetch(`https://ahsuite.com/api/portal/file/list?${params}`, {
headers: { Authorization: `Basic ${auth}` },
});
console.log(await res.json());import requests
res = requests.get(
'https://ahsuite.com/api/portal/file/list',
params={'portal_id': 'vJW4', 'page': 1, 'limit': 20},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "",
"data": {
"files": [
{
"title": "Brand Assets",
"type": "folder",
"status": "",
"share_with_client": 1,
"created_date": "2026-05-20 08:15:40",
"file_id": "6mNs"
},
{
"title": "logo-final.png",
"type": "upload",
"status": "client_approved",
"share_with_client": 1,
"created_date": "2026-06-01 12:41:08",
"file_id": "7pQw"
},
{
"title": "Signed contract",
"type": "request",
"status": "request_file",
"share_with_client": 1,
"created_date": "2026-06-03 09:05:22",
"file_id": "8rTx"
}
],
"pagination": {
"total": 9,
"page": 1,
"limit": 20,
"total_pages": 1
}
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"portal_id\":[\"The portal id field is required.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Folder record not found."
}File Detail
Return a single file, folder or file request in full — title, description, status, sharing, its versions (newest first), comments and labels, as shown on the file screen in the app. Field names match List Files and Create File. For a folder only its own details are returned (is_folder = 1, sharing, parent folder_id, items_count); to see the files inside it call List Files with folder_id. Archived files are not returned.
404.Other unsuccessful response codes: 403, 429, 500.
curl --request GET \
-u "EMAIL:TOKEN" \
'https://ahsuite.com/api/portal/file/detail?portal_id=vJW4&file_id=7pQ1'$params = http_build_query([
'portal_id' => 'vJW4',
'file_id' => '7pQ1',
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/file/detail?'.$params);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const params = new URLSearchParams({ portal_id: 'vJW4', file_id: '7pQ1' });
const res = await fetch(`https://ahsuite.com/api/portal/file/detail?${params}`, {
headers: { Authorization: `Basic ${auth}` },
});
console.log(await res.json());import requests
res = requests.get(
'https://ahsuite.com/api/portal/file/detail',
params={'portal_id': 'vJW4', 'file_id': '7pQ1'},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "",
"data": {
"title": "Signed contract",
"description": "",
"is_folder": 0,
"is_request": 0,
"status": "client_approved",
"require_client_approval": 1,
"share_with_client": 1,
"share_with_team": 1,
"folder_id": "6kFz",
"created_date": "2026-06-02 14:22:05",
"file_id": "7pQ1",
"versions": [
{ "file_name": "contract-v2.pdf", "type": "file", "url": "https://s3.amazonaws.com/.../tool/API-1718000000-contract-v2.pdf?X-Amz-Expires=7200&...", "approved_by": "Sam Lee", "approved_date": "2026-06-10 09:15:02", "created_date": "2026-06-09 17:40:11", "version_id": 9012 },
{ "file_name": "contract.pdf", "type": "file", "url": "https://s3.amazonaws.com/.../tool/API-1717000000-contract.pdf?X-Amz-Expires=7200&...", "approved_by": "", "approved_date": null, "created_date": "2026-06-02 14:22:05", "version_id": 8990 }
],
"comments": [
{ "comment": "Please sign page 3 as well.", "by": "Jane Cooper", "from": "agent", "created_date": "2026-06-08 11:02:40", "comment_id": 3301 }
],
"labels": [
{ "name": "Legal", "color": "purple" }
]
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"file_id\":[\"The file id field is required.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "File record not found."
}Update a File
Update a file, file request or folder. This is a partial update: only the parameters you send are changed. Each change behaves exactly like editing the field on the file screen — turning require_client_approval on shares the file with the client (and its folder, if any), sets it to "approval required" and notifies them by email (the "Action required" email, or "File required" for a file request); it cannot be changed on a file the client already approved. Folders accept title and description only.
404.p, br, strong, em, ul, ol, li, a. Max 5000 characters. Empty string clears it.1 asks the client to review and approve the file (shares it with them), 0 removes the approval requirement. On a file request this is the "Request File" toggle: 1 asks the client to upload the file. Not accepted for folders; returns 400 once the client has approved / submitted.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "file_id=7pQ1" \
--data-urlencode "title=Signed contract (final)" \
--data-urlencode "require_client_approval=1" \
'https://ahsuite.com/api/portal/file/update'$ch = curl_init();
$data = [
'portal_id' => 'vJW4',
'file_id' => '7pQ1',
'title' => 'Signed contract (final)',
'require_client_approval' => 1,
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/file/update');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams({ portal_id: 'vJW4', file_id: '7pQ1', title: 'Signed contract (final)', require_client_approval: '1' });
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/file/update', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/file/update',
data={'portal_id': 'vJW4', 'file_id': '7pQ1', 'title': 'Signed contract (final)', 'require_client_approval': 1},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "File has been updated successfully.",
"data": {
"file_id": "7pQ1"
}
}{
"success": false,
"message": "Validation Error",
"data": "Oop's this document is already approved!!"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "File record not found."
}Add a File Version
Add a new version to an existing file — the same as Add a file on the file screen. Send either one uploaded file (multipart/form-data) or one url per request. Older versions are kept; if the client had already approved the file it goes back to "approval required". The addition is written to the file history. A file can hold at most 70 versions.
multipart/form-data). Same allowed types as Create File; max 50 MB. Not available through the MCP connector. Send either attachment_file or url, not both.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
-F "portal_id=vJW4" \
-F "file_id=7pQ1" \
-F "attachment_file=@/path/to/contract-v3.pdf" \
'https://ahsuite.com/api/portal/file/version/add'$ch = curl_init();
$data = [
'portal_id' => 'vJW4',
'file_id' => '7pQ1',
'attachment_file' => new CURLFile('/path/to/contract-v3.pdf'),
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/file/version/add');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
// link version — for an upload use FormData with an 'attachment_file' file instead
const body = new URLSearchParams({ portal_id: 'vJW4', file_id: '7pQ1', url: 'https://drive.google.com/file/d/1AbC/view' });
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/file/version/add', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/file/version/add',
data={'portal_id': 'vJW4', 'file_id': '7pQ1'},
files={'attachment_file': open('/path/to/contract-v3.pdf', 'rb')},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Version has been added successfully.",
"data": {
"file_id": "7pQ1",
"version_id": 9013
}
}{
"success": false,
"message": "Validation Error",
"data": "Send exactly one of: attachment_file (file) or url."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "File record not found."
}Add a File Comment
Post a comment on a file or file request — exactly like commenting on the file screen: the comment is written to the file history and the people involved are notified (client users are notified when the file is shared with them). Basic HTML is allowed. Folders cannot be commented on.
p, br, strong, em, ul, ol, li, a; other tags are stripped.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "file_id=7pQ1" \
--data-urlencode "comment=Please sign page 3 as well." \
'https://ahsuite.com/api/portal/file/comment/add'$ch = curl_init();
$data = [
'portal_id' => 'vJW4',
'file_id' => '7pQ1',
'comment' => 'Please sign page 3 as well.',
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/file/comment/add');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams({ portal_id: 'vJW4', file_id: '7pQ1', comment: 'Please sign page 3 as well.' });
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/file/comment/add', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/file/comment/add',
data={'portal_id': 'vJW4', 'file_id': '7pQ1', 'comment': 'Please sign page 3 as well.'},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Comment has been added successfully.",
"data": {
"file_id": "7pQ1",
"comment_id": 3302
}
}{
"success": false,
"message": "Validation Error",
"data": "Comments cannot be added to a folder."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "File record not found."
}Move a File
Move a file into a folder of the same portal, or back to the top level — the same as moving it in the app. The file takes over the sharing of the folder it lands in (folder members and client users). Folders themselves cannot be moved.
file_id from List Files (a row with type = folder). Send an empty string to move the file to the root. A folder of another portal, or one you are not assigned to, returns 404.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "file_id=7pQ1" \
--data-urlencode "folder_id=6kFz" \
'https://ahsuite.com/api/portal/file/move'$ch = curl_init();
$data = [
'portal_id' => 'vJW4',
'file_id' => '7pQ1',
'folder_id' => '6kFz',
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/file/move');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams({ portal_id: 'vJW4', file_id: '7pQ1', folder_id: '6kFz' });
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/file/move', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/file/move',
data={'portal_id': 'vJW4', 'file_id': '7pQ1', 'folder_id': '6kFz'},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "File has been moved successfully.",
"data": {
"file_id": "7pQ1",
"folder_id": "6kFz"
}
}{
"success": false,
"message": "Validation Error",
"data": "Can be moved in a folder only."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Folder record not found."
}Add File Labels
Add one or more labels to a file or file request — the same as Add Label on the file screen. A label with the same name and color already in the portal is reused, otherwise it is created. Labels already on the file are kept. Folders cannot be labelled.
green, yellow, orange, red, purple, blue, sky, lime, pink or black. Omit for no color.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "file_id=7pQ1" \
--data-urlencode "labels[0][name]=Legal" \
--data-urlencode "labels[0][color]=purple" \
'https://ahsuite.com/api/portal/file/label/add'$ch = curl_init();
$data = [
'portal_id' => 'vJW4',
'file_id' => '7pQ1',
'labels' => [
['name' => 'Legal', 'color' => 'purple'],
],
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/file/label/add');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams({
portal_id: 'vJW4',
file_id: '7pQ1',
'labels[0][name]': 'Legal',
'labels[0][color]': 'purple',
});
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/file/label/add', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/file/label/add',
data={'portal_id': 'vJW4', 'file_id': '7pQ1', 'labels[0][name]': 'Legal', 'labels[0][color]': 'purple'},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Labels have been added successfully.",
"data": {
"file_id": "7pQ1"
}
}{
"success": false,
"message": "Validation Error",
"data": "These labels are already on the file — nothing to change."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "File record not found."
}Create a Journal
Create a new work journal inside an existing portal. Requires a valid portal_id. Returns the new journal_id on success.
Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
'https://ahsuite.com/api/portal/journal/create?portal_id=PORTAL_ID&name=Journal+Name'$ch = curl_init();
$data = [ 'portal_id' => 'PORTAL_ID', 'name' => 'Journal Name' ];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/journal/create');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const url = new URL('https://ahsuite.com/api/portal/journal/create');
url.searchParams.set('portal_id', 'PORTAL_ID');
url.searchParams.set('name', 'Journal Name');
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch(url, {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/journal/create',
auth=('EMAIL', 'TOKEN'),
params={'portal_id': 'PORTAL_ID', 'name': 'Journal Name'},
)
print(res.json()){
"success": true,
"message": "Journal has been added successfully.",
"data": {
"journal_id": "vJW4"
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"name\":[\"The name field is required.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Portal record not found."
}List Journals
Return the work journals of a portal, newest first. Archived journals are excluded. Results are paginated.
1.20, maximum 100.Other unsuccessful response codes: 403, 429, 500.
curl --request GET \
-u "EMAIL:TOKEN" \
'https://ahsuite.com/api/portal/journal/list?portal_id=vJW4&page=1&limit=20'$params = http_build_query([
'portal_id' => 'vJW4',
'page' => 1,
'limit' => 20,
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/journal/list?'.$params);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const params = new URLSearchParams({ portal_id: 'vJW4', page: 1, limit: 20 });
const res = await fetch(`https://ahsuite.com/api/portal/journal/list?${params}`, {
headers: { Authorization: `Basic ${auth}` },
});
console.log(await res.json());import requests
res = requests.get(
'https://ahsuite.com/api/portal/journal/list',
params={'portal_id': 'vJW4', 'page': 1, 'limit': 20},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "",
"data": {
"journals": [
{
"name": "June development log",
"created_date": "2026-06-01 08:00:12",
"journal_id": 54
},
{
"name": "Weekly SEO notes",
"created_date": "2026-05-12 10:30:45",
"journal_id": 47
}
],
"pagination": {
"total": 5,
"page": 1,
"limit": 20,
"total_pages": 1
}
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"portal_id\":[\"The portal id field is required.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Portal record not found."
}Journal Detail
Return a single journal in full — its name, created date and the newest 25 log entries, newest first, as shown in the journal popup in the app. Log entries are HTML; images pasted into a log come back with a temporary link that is valid for 15 minutes. Archived journals are not returned. Get a journal_id from List Journals.
404.Other unsuccessful response codes: 403, 429, 500.
curl --request GET \
-u "EMAIL:TOKEN" \
'https://ahsuite.com/api/portal/journal/detail?portal_id=vJW4&journal_id=60'$params = http_build_query([
'portal_id' => 'vJW4',
'journal_id' => 60,
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/journal/detail?'.$params);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const params = new URLSearchParams({ portal_id: 'vJW4', journal_id: '60' });
const res = await fetch(`https://ahsuite.com/api/portal/journal/detail?${params}`, {
headers: { Authorization: `Basic ${auth}` },
});
console.log(await res.json());import requests
res = requests.get(
'https://ahsuite.com/api/portal/journal/detail',
params={'portal_id': 'vJW4', 'journal_id': 60},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "",
"data": {
"name": "Website redesign",
"created_date": "2026-06-02 14:20:11",
"journal_id": 60,
"logs": [
{ "log": "<p>Homepage wireframe approved by the client.</p>", "by": "Jane Cooper", "created_date": "2026-06-10 09:15:02", "log_id": 412 },
{ "log": "<p>Kickoff call done, notes shared.</p>", "by": "Jane Cooper", "created_date": "2026-06-03 17:40:11", "log_id": 401 }
]
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"journal_id\":[\"The journal id field is required.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Journal record not found."
}Update a Journal
Rename a journal. The name is the only field of a journal that can be edited in the app, so it is the only field here; log entries are added with Add a Journal Log. Get a journal_id from List Journals.
404.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "journal_id=60" \
--data-urlencode "name=Website redesign v2" \
'https://ahsuite.com/api/portal/journal/update'$ch = curl_init();
$data = [
'portal_id' => 'vJW4',
'journal_id' => 60,
'name' => 'Website redesign v2',
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/journal/update');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams();
body.append('portal_id', 'vJW4');
body.append('journal_id', '60');
body.append('name', 'Website redesign v2');
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/journal/update', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/journal/update',
data={'portal_id': 'vJW4', 'journal_id': 60, 'name': 'Website redesign v2'},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Journal has been updated.",
"data": {
"journal_id": 60,
"name": "Website redesign v2"
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"name\":[\"The name field is required.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Journal record not found."
}Add a Journal Log
Add a new log entry to a journal — the same as Save Log in the journal popup. The API owner is recorded as the author. Mention a team member with @name (their username, as shown in the app) and they are notified, as in the app. Text only: pasted images are not accepted here. A journal holds up to 200 logs. Get a journal_id from List Journals.
404.p, br, strong, em, ul, ol, li, a); other tags are removed.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "journal_id=60" \
--data-urlencode "log=Kickoff call done, notes shared with @jane." \
'https://ahsuite.com/api/portal/journal/log/add'$ch = curl_init();
$data = [
'portal_id' => 'vJW4',
'journal_id' => 60,
'log' => 'Kickoff call done, notes shared with @jane.',
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/journal/log/add');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams();
body.append('portal_id', 'vJW4');
body.append('journal_id', '60');
body.append('log', 'Kickoff call done, notes shared with @jane.');
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/journal/log/add', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/journal/log/add',
data={'portal_id': 'vJW4', 'journal_id': 60, 'log': 'Kickoff call done, notes shared with @jane.'},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Log has been added to the journal.",
"data": {
"journal_id": 60,
"log_id": 413
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"log\":[\"The log field is required.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Journal record not found."
}Save a Login
Save a login's name, address and username in a portal's Password Manager. This endpoint never takes a password — any field outside the ones below (including password) saves nothing and returns 400. Without options the login is saved as Password needed and the response carries add_password_url, the page in Ahsuite where the password is added. With request, Ahsuite emails that client user to add the password in their portal. With generate, Ahsuite creates a strong 20-character password that can be revealed in Ahsuite (it is never returned here).
WordPress admin. Max 255 characters.1 to make the login visible to the client. Default 0.1 to share the login with your internal team. Default 0 (only you).generate.1 and Ahsuite generates a strong password for this new login (letters, digits and symbols). The value is stored encrypted and never returned by the API. Cannot be combined with request.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
'https://ahsuite.com/api/portal/password/create' \
--data-urlencode 'portal_id=PORTAL_ID' \
--data-urlencode 'name=Hosting cPanel' \
--data-urlencode 'url=https://example.com/login' \
--data-urlencode 'username=admin' \
--data-urlencode 'share_with_client=0' \
--data-urlencode 'request[client_user_email]=sam@client.com' \
--data-urlencode 'request[message]=Please add the cPanel password here.'$ch = curl_init();
$data = [
'portal_id' => 'PORTAL_ID',
'name' => 'Hosting cPanel',
'url' => 'https://example.com/login',
'username' => 'admin',
'share_with_client' => 0,
'request' => [
'client_user_email' => 'sam@client.com',
'message' => 'Please add the cPanel password here.',
],
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/password/create');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams({
portal_id: 'PORTAL_ID',
name: 'Hosting cPanel',
url: 'https://example.com/login',
username: 'admin',
share_with_client: '0',
'request[client_user_email]': 'sam@client.com',
'request[message]': 'Please add the cPanel password here.',
});
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/password/create', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/password/create',
auth=('EMAIL', 'TOKEN'),
data={
'portal_id': 'PORTAL_ID',
'name': 'Hosting cPanel',
'url': 'https://example.com/login',
'username': 'admin',
'share_with_client': 0,
'request[client_user_email]': 'sam@client.com',
'request[message]': 'Please add the cPanel password here.',
},
)
print(res.json())// without options — password still needed
{
"success": true,
"message": "Login saved without a password. The user adds the password in Ahsuite at add_password_url.",
"data": {
"password_id": "vJW4",
"password_status": "needed",
"add_password_url": "https://ahsuite.com/agent/client/tool/account-access/edit/NdLR/vJW4"
}
}
// with request
{
"success": true,
"message": "Login saved. Sam has been asked to add the password in Ahsuite.",
"data": {
"password_id": "vJW4",
"password_status": "requested",
"request": {
"to": "Sam",
"to_email": "sam@client.com",
"sent_at": "2026-10-13T09:15:30-07:00"
}
}
}
// with generate
{
"success": true,
"message": "Login saved with a strong generated password. The user can reveal it in Ahsuite at login_url.",
"data": {
"password_id": "vJW4",
"password_status": "set",
"login_url": "https://ahsuite.com/agent/client/tool/account-access/edit/NdLR/vJW4"
}
}// a password was sent — nothing is saved
{
"success": false,
"message": "Validation Error",
"data": "Passwords can't go through the assistant, so nothing was saved. Save the login without its password; the user then adds the password in Ahsuite. If the user typed a password in this chat, tell them it is now in the chat history."
}
// other refusals
{ "success": false, "message": "Validation Error", "data": "That person isn't a client user of this portal." }
{ "success": false, "message": "Validation Error", "data": "This portal has sent 20 password requests today. Try again tomorrow." }
{ "success": false, "message": "Validation Error", "data": "Send either request or generate, not both." }
{ "success": false, "message": "Validation Error", "data": "{\"name\":[\"The name field is required.\"]}" }{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Portal record not found."
}List Logins
Return the saved logins of a portal, newest first — name, address, username, sharing, password_status (needed, requested or set), when the password last changed and by whom, the open request when there is one, and add_password_url while the password is still needed. All dates are ISO 8601 in the portal's timezone. No field ever contains a password or any part of one; passwords are only viewed in the Ahsuite app. Archived logins are excluded. Results are paginated.
1.20, maximum 100.Other unsuccessful response codes: 403, 429, 500.
curl --request GET \
-u "EMAIL:TOKEN" \
'https://ahsuite.com/api/portal/password/list?portal_id=vJW4&page=1&limit=20'$params = http_build_query([
'portal_id' => 'vJW4',
'page' => 1,
'limit' => 20,
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/password/list?'.$params);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const params = new URLSearchParams({ portal_id: 'vJW4', page: 1, limit: 20 });
const res = await fetch(`https://ahsuite.com/api/portal/password/list?${params}`, {
headers: { Authorization: `Basic ${auth}` },
});
console.log(await res.json());import requests
res = requests.get(
'https://ahsuite.com/api/portal/password/list',
params={'portal_id': 'vJW4', 'page': 1, 'limit': 20},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "",
"data": {
"passwords": [
{
"password_id": "vJW4",
"name": "WordPress admin",
"url": "https://clientsite.com/wp-admin",
"username": "admin",
"share_with_client": 1,
"share_with_team": 0,
"password_status": "requested",
"password_changed_at": null,
"password_changed_by": "",
"created_date": "2026-10-13T09:15:30-07:00",
"request": {
"to": "Sam",
"requested_by": "Kevin Frei",
"sent_at": "2026-10-13T09:15:30-07:00"
}
},
{
"password_id": "eRq2",
"name": "Hosting cPanel",
"url": "https://host.com:2083",
"username": "client",
"share_with_client": 0,
"share_with_team": 1,
"password_status": "set",
"password_changed_at": "2026-10-09T14:22:05-07:00",
"password_changed_by": "Kevin Frei",
"created_date": "2026-05-02T14:22:05-07:00"
},
{
"password_id": "kL9m",
"name": "Analytics",
"url": "https://analytics.example.com",
"username": "",
"share_with_client": 0,
"share_with_team": 0,
"password_status": "needed",
"password_changed_at": null,
"password_changed_by": "",
"created_date": "2026-10-12T16:40:00-07:00",
"add_password_url": "https://ahsuite.com/agent/client/tool/account-access/edit/NdLR/kL9m"
}
],
"pagination": {
"total": 8,
"page": 1,
"limit": 20,
"total_pages": 1
}
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"portal_id\":[\"The portal id field is required.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Portal record not found."
}Create an Invoice
Create a new invoice inside an existing portal with one or more line items, each with optional add/subtract sub-items (e.g. taxes, fees, discounts). The invoice total is computed automatically from the items — you do not send a total. Client users who have email notifications enabled are notified by email. Returns the new invoice_id and invoice_number on success.
YYYY-MM-DD.YYYY-MM-DD.USD (default) or GBP.items[] required (max 50)add (e.g. tax, fee) or subtract (e.g. discount). Required for each sub-item. Max 20 sub-items per item.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
'https://ahsuite.com/api/portal/invoice/create' \
--data-urlencode 'portal_id=PORTAL_ID' \
--data-urlencode 'invoice_date=2026-07-08' \
--data-urlencode 'due_date=2026-07-22' \
--data-urlencode 'items[0][name]=Website design' \
--data-urlencode 'items[0][quantity]=1' \
--data-urlencode 'items[0][price]=500' \
--data-urlencode 'items[0][sub_items][0][type]=add' \
--data-urlencode 'items[0][sub_items][0][description]=VAT 20%' \
--data-urlencode 'items[0][sub_items][0][amount]=100'$ch = curl_init();
$data = [
'portal_id' => 'PORTAL_ID',
'invoice_date' => '2026-07-08',
'due_date' => '2026-07-22',
'items' => [
[
'name' => 'Website design',
'quantity' => 1,
'price' => 500,
'sub_items' => [
[ 'type' => 'add', 'description' => 'VAT 20%', 'amount' => 100 ],
],
],
],
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/invoice/create');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams({
portal_id: 'PORTAL_ID',
invoice_date: '2026-07-08',
due_date: '2026-07-22',
'items[0][name]': 'Website design',
'items[0][quantity]': '1',
'items[0][price]': '500',
'items[0][sub_items][0][type]': 'add',
'items[0][sub_items][0][description]': 'VAT 20%',
'items[0][sub_items][0][amount]': '100',
});
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/invoice/create', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/invoice/create',
auth=('EMAIL', 'TOKEN'),
data={
'portal_id': 'PORTAL_ID',
'invoice_date': '2026-07-08',
'due_date': '2026-07-22',
'items[0][name]': 'Website design',
'items[0][quantity]': 1,
'items[0][price]': 500,
'items[0][sub_items][0][type]': 'add',
'items[0][sub_items][0][description]': 'VAT 20%',
'items[0][sub_items][0][amount]': 100,
},
)
print(res.json()){
"success": true,
"message": "Invoice has been added successfully.",
"data": {
"invoice_id": 55,
"invoice_number": "00006"
}
}{
"success": false,
"message": "Validation Error",
"data": "Invoice number 00006 already exists for this portal. Please use a different number."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Portal record not found."
}List Invoices
Return the invoices of a portal, newest first — number, dates, amount, currency and payment status. Archived invoices are excluded. Results are paginated.
1.20, maximum 100.Invoice status is one of: due, paid, payment_processing, cancelled, refunded. Other unsuccessful response codes: 403, 429, 500.
curl --request GET \
-u "EMAIL:TOKEN" \
'https://ahsuite.com/api/portal/invoice/list?portal_id=vJW4&page=1&limit=20'$params = http_build_query([
'portal_id' => 'vJW4',
'page' => 1,
'limit' => 20,
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/invoice/list?'.$params);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const params = new URLSearchParams({ portal_id: 'vJW4', page: 1, limit: 20 });
const res = await fetch(`https://ahsuite.com/api/portal/invoice/list?${params}`, {
headers: { Authorization: `Basic ${auth}` },
});
console.log(await res.json());import requests
res = requests.get(
'https://ahsuite.com/api/portal/invoice/list',
params={'portal_id': 'vJW4', 'page': 1, 'limit': 20},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "",
"data": {
"invoices": [
{
"invoice_number": "00012",
"invoice_date": "2026-08-01",
"due_date": "2026-08-15",
"total_amount": "450.00",
"currency": "USD",
"status": "due",
"created_date": "2026-08-01 10:12:44",
"invoice_id": 55
},
{
"invoice_number": "00011",
"invoice_date": "2026-07-01",
"due_date": "2026-07-15",
"total_amount": "450.00",
"currency": "USD",
"status": "paid",
"created_date": "2026-07-01 09:03:21",
"invoice_id": 54
}
],
"pagination": {
"total": 12,
"page": 1,
"limit": 20,
"total_pages": 1
}
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"portal_id\":[\"The portal id field is required.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Portal record not found."
}Invoice Detail
Return a single invoice in full — number, dates, currency, status, total, notes and every line item with its add / subtract sub-items, as shown on the invoice screen in the app. Field names match Create Invoice. Archived invoices are not returned. Get an invoice_id from List Invoices.
404.Other unsuccessful response codes: 403, 429, 500.
curl --request GET \
-u "EMAIL:TOKEN" \
'https://ahsuite.com/api/portal/invoice/detail?portal_id=vJW4&invoice_id=55'$params = http_build_query([
'portal_id' => 'vJW4',
'invoice_id' => 55,
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/invoice/detail?'.$params);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const params = new URLSearchParams({ portal_id: 'vJW4', invoice_id: '55' });
const res = await fetch(`https://ahsuite.com/api/portal/invoice/detail?${params}`, {
headers: { Authorization: `Basic ${auth}` },
});
console.log(await res.json());import requests
res = requests.get(
'https://ahsuite.com/api/portal/invoice/detail',
params={'portal_id': 'vJW4', 'invoice_id': 55},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "",
"data": {
"invoice_number": "00011",
"invoice_date": "2026-06-01",
"due_date": "2026-06-15",
"currency": "USD",
"status": "due",
"total_amount": "1150.00",
"notes": "Payable within 14 days.",
"created_date": "2026-06-01 10:02:44",
"invoice_id": 55,
"items": [
{
"name": "Website design",
"description": "Homepage and 3 inner pages",
"quantity": 1,
"price": "1000.00",
"line_total": "1150.00",
"item_id": 912,
"sub_items": [
{ "type": "add", "description": "Rush delivery", "amount": "200.00", "sub_item_id": 3300 },
{ "type": "subtract", "description": "Loyalty discount", "amount": "50.00", "sub_item_id": 3301 }
]
}
]
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"invoice_id\":[\"The invoice id field is required.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Invoice record not found."
}Update an Invoice
Edit the header of an unpaid invoice — number, dates, currency and notes. Paid, processing, cancelled and refunded invoices cannot be edited. This is a partial update: only the parameters you send are changed. Line items are not changed here; use Add an Invoice Item and Update an Invoice Item. No email is sent to the client. Get an invoice_id from List Invoices.
404; an invoice that is not unpaid returns 400.YYYY-MM-DD.YYYY-MM-DD.USD or GBP.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "invoice_id=55" \
--data-urlencode "due_date=2026-10-30" \
--data-urlencode "notes=Payable within 30 days." \
'https://ahsuite.com/api/portal/invoice/update'$ch = curl_init();
$data = [
'portal_id' => 'vJW4',
'invoice_id' => 55,
'due_date' => '2026-10-30',
'notes' => 'Payable within 30 days.',
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/invoice/update');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams();
body.append('portal_id', 'vJW4');
body.append('invoice_id', '55');
body.append('due_date', '2026-10-30');
body.append('notes', 'Payable within 30 days.');
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/invoice/update', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/invoice/update',
data={'portal_id': 'vJW4', 'invoice_id': 55, 'due_date': '2026-10-30', 'notes': 'Payable within 30 days.'},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Invoice has been updated.",
"data": {
"invoice_id": 55,
"invoice_number": "00011",
"total_amount": "1150.00"
}
}{
"success": false,
"message": "Validation Error",
"data": "Only unpaid invoices can be edited."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Invoice record not found."
}Add an Invoice Item
Add one line item to an unpaid invoice — the same as + Add Another Item on the edit form. The item can carry add / subtract adjustments (modifications). Existing items are left untouched and the invoice total is recomputed. An invoice holds up to 50 items. Get an invoice_id from List Invoices.
400.sub_items[0][type] (add / subtract), sub_items[0][amount], optional sub_items[0][description].Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "invoice_id=55" \
--data-urlencode "name=Logo design" \
--data-urlencode "quantity=1" \
--data-urlencode "price=300" \
--data-urlencode "sub_items[0][type]=subtract" \
--data-urlencode "sub_items[0][description]=Loyalty discount" \
--data-urlencode "sub_items[0][amount]=50" \
'https://ahsuite.com/api/portal/invoice/item/add'$ch = curl_init();
$data = [
'portal_id' => 'vJW4',
'invoice_id' => 55,
'name' => 'Logo design',
'quantity' => 1,
'price' => 300,
'sub_items' => [['type' => 'subtract', 'description' => 'Loyalty discount', 'amount' => 50]],
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/invoice/item/add');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams();
body.append('portal_id', 'vJW4');
body.append('invoice_id', '55');
body.append('name', 'Logo design');
body.append('quantity', '1');
body.append('price', '300');
body.append('sub_items[0][type]', 'subtract');
body.append('sub_items[0][description]', 'Loyalty discount');
body.append('sub_items[0][amount]', '50');
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/invoice/item/add', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/invoice/item/add',
data={
'portal_id': 'vJW4', 'invoice_id': 55, 'name': 'Logo design', 'quantity': 1, 'price': 300,
'sub_items[0][type]': 'subtract', 'sub_items[0][description]': 'Loyalty discount', 'sub_items[0][amount]': 50,
},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Item has been added to the invoice.",
"data": {
"invoice_id": 55,
"item_id": 912,
"total_amount": "1400.00"
}
}{
"success": false,
"message": "Validation Error",
"data": "Only unpaid invoices can be edited."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Invoice record not found."
}Update an Invoice Item
Edit one line item of an unpaid invoice. This is a partial update: only the fields you send are changed. Adjustments (sub_items[]) can be edited by passing their sub_item_id, or appended by leaving it out. To delete an item use Remove an Invoice Item; to delete an adjustment use Remove an Invoice Adjustment. The invoice total is recomputed. Get item_id and sub_item_id from Invoice Detail.
400.sub_items[0][sub_item_id] the existing adjustment is edited (send only the fields to change). Without it a new adjustment is appended and [type] (add / subtract) plus [amount] are required; [description] is optional.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "invoice_id=55" \
--data-urlencode "item_id=912" \
--data-urlencode "quantity=2" \
--data-urlencode "sub_items[0][sub_item_id]=3301" \
--data-urlencode "sub_items[0][amount]=75" \
--data-urlencode "sub_items[1][type]=add" \
--data-urlencode "sub_items[1][description]=Rush delivery" \
--data-urlencode "sub_items[1][amount]=100" \
'https://ahsuite.com/api/portal/invoice/item/update'$ch = curl_init();
$data = [
'portal_id' => 'vJW4',
'invoice_id' => 55,
'item_id' => 912,
'quantity' => 2,
'sub_items' => [
['sub_item_id' => 3301, 'amount' => 75],
['type' => 'add', 'description' => 'Rush delivery', 'amount' => 100],
],
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/invoice/item/update');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams();
body.append('portal_id', 'vJW4');
body.append('invoice_id', '55');
body.append('item_id', '912');
body.append('quantity', '2');
body.append('sub_items[0][sub_item_id]', '3301');
body.append('sub_items[0][amount]', '75');
body.append('sub_items[1][type]', 'add');
body.append('sub_items[1][description]', 'Rush delivery');
body.append('sub_items[1][amount]', '100');
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/invoice/item/update', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/invoice/item/update',
data={
'portal_id': 'vJW4', 'invoice_id': 55, 'item_id': 912, 'quantity': 2,
'sub_items[0][sub_item_id]': 3301, 'sub_items[0][amount]': 75,
'sub_items[1][type]': 'add', 'sub_items[1][description]': 'Rush delivery', 'sub_items[1][amount]': 100,
},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Invoice item has been updated.",
"data": {
"invoice_id": 55,
"item_id": 912,
"total_amount": "2225.00"
}
}{
"success": false,
"message": "Validation Error",
"data": "Nothing to update. Send at least one of: name, description, quantity, price, sub_items."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Item not found on this invoice."
}Remove an Invoice Item
Remove one line item from an unpaid invoice together with all its adjustments — the same as Remove Item on the edit form. To remove a single adjustment and keep the item, use Remove an Invoice Adjustment. This cannot be undone. The invoice total is recomputed. Get item_id from Invoice Detail.
400.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "invoice_id=55" \
--data-urlencode "item_id=912" \
'https://ahsuite.com/api/portal/invoice/item/remove'$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/invoice/item/remove');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(['portal_id' => 'vJW4', 'invoice_id' => 55, 'item_id' => 912]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams();
body.append('portal_id', 'vJW4');
body.append('invoice_id', '55');
body.append('item_id', '912');
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/invoice/item/remove', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/invoice/item/remove',
data={'portal_id': 'vJW4', 'invoice_id': 55, 'item_id': 912},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Item has been removed from the invoice.",
"data": {
"invoice_id": 55,
"item_id": 912,
"total_amount": "0.00"
}
}{
"success": false,
"message": "Validation Error",
"data": "Only unpaid invoices can be edited."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Item not found on this invoice."
}Remove an Invoice Adjustment
Remove one add / subtract adjustment (modification) from a line item of an unpaid invoice — the trash icon next to a modification on the edit form. The item itself stays. This cannot be undone. The invoice total is recomputed. Get item_id and sub_item_id from Invoice Detail.
400.items[].sub_items[].sub_item_id. An adjustment of another item returns 404.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "invoice_id=55" \
--data-urlencode "item_id=912" \
--data-urlencode "sub_item_id=3301" \
'https://ahsuite.com/api/portal/invoice/item/adjustment/remove'$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/invoice/item/adjustment/remove');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(['portal_id' => 'vJW4', 'invoice_id' => 55, 'item_id' => 912, 'sub_item_id' => 3301]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams();
body.append('portal_id', 'vJW4');
body.append('invoice_id', '55');
body.append('item_id', '912');
body.append('sub_item_id', '3301');
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/invoice/item/adjustment/remove', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/invoice/item/adjustment/remove',
data={'portal_id': 'vJW4', 'invoice_id': 55, 'item_id': 912, 'sub_item_id': 3301},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Adjustment has been removed from the item.",
"data": {
"invoice_id": 55,
"item_id": 912,
"sub_item_id": 3301,
"total_amount": "1200.00"
}
}{
"success": false,
"message": "Validation Error",
"data": "Only unpaid invoices can be edited."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Adjustment not found on this item."
}Mark an Invoice Paid
Mark an unpaid invoice as paid (manual payment), the same as the Mark Paid button on the invoice screen. Only unpaid invoices can be marked paid, and this cannot be undone through the API. No email is sent. Get an invoice_id from List Invoices.
400.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "invoice_id=55" \
'https://ahsuite.com/api/portal/invoice/mark-paid'$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/invoice/mark-paid');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(['portal_id' => 'vJW4', 'invoice_id' => 55]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams();
body.append('portal_id', 'vJW4');
body.append('invoice_id', '55');
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/invoice/mark-paid', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/invoice/mark-paid',
data={'portal_id': 'vJW4', 'invoice_id': 55},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Invoice has been marked as paid.",
"data": {
"invoice_id": 55,
"invoice_number": "00011",
"status": "paid"
}
}{
"success": false,
"message": "Validation Error",
"data": "This invoice is already paid."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Invoice record not found."
}Create a Project
Create a new project board inside an existing portal. Requires a valid portal_id. Returns the new project_id on success.
1 to make the project visible to the client. Default 0.1 to share the project with your internal team. Default 0 (only you).Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
'https://ahsuite.com/api/portal/project/create' \
--data-urlencode 'portal_id=PORTAL_ID' \
--data-urlencode 'name=Website Redesign' \
--data-urlencode 'description=Full redesign of the marketing site' \
--data-urlencode 'share_with_client=1'$ch = curl_init();
$data = [
'portal_id' => 'PORTAL_ID',
'name' => 'Website Redesign',
'description' => 'Full redesign of the marketing site',
'share_with_client' => 1,
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/project/create');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams({
portal_id: 'PORTAL_ID',
name: 'Website Redesign',
description: 'Full redesign of the marketing site',
share_with_client: '1',
});
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/project/create', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/project/create',
auth=('EMAIL', 'TOKEN'),
data={
'portal_id': 'PORTAL_ID',
'name': 'Website Redesign',
'description': 'Full redesign of the marketing site',
'share_with_client': 1,
},
)
print(res.json()){
"success": true,
"message": "Project has been added successfully.",
"data": {
"project_id": "vJW4"
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"name\":[\"The name field is required.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Portal record not found."
}List Projects
Return the projects of a portal in the same order as the app (manual position, then newest first). Archived projects are excluded. Results are paginated.
1.20, maximum 100.Other unsuccessful response codes: 403, 429, 500.
curl --request GET \
-u "EMAIL:TOKEN" \
'https://ahsuite.com/api/portal/project/list?portal_id=vJW4&page=1&limit=20'$params = http_build_query([
'portal_id' => 'vJW4',
'page' => 1,
'limit' => 20,
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/project/list?'.$params);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const params = new URLSearchParams({ portal_id: 'vJW4', page: 1, limit: 20 });
const res = await fetch(`https://ahsuite.com/api/portal/project/list?${params}`, {
headers: { Authorization: `Basic ${auth}` },
});
console.log(await res.json());import requests
res = requests.get(
'https://ahsuite.com/api/portal/project/list',
params={'portal_id': 'vJW4', 'page': 1, 'limit': 20},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "",
"data": {
"projects": [
{
"name": "Website redesign",
"share_with_client": 1,
"created_date": "2026-07-14 11:20:10",
"project_id": 82
},
{
"name": "SEO retainer",
"share_with_client": 0,
"created_date": "2026-06-02 15:44:37",
"project_id": 75
}
],
"pagination": {
"total": 6,
"page": 1,
"limit": 20,
"total_pages": 1
}
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"portal_id\":[\"The portal id field is required.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Portal record not found."
}Project Detail
Return a single project in full — name, description, sharing, its tasks and phases in the order the project screen shows them, and its resources (linked pages, files, conversations and passwords). Task rows use the same fields as List Tasks; resource ids match the other list endpoints. Archived projects are not returned.
404.Other unsuccessful response codes: 403, 429, 500.
curl --request GET \
-u "EMAIL:TOKEN" \
'https://ahsuite.com/api/portal/project/detail?portal_id=vJW4&project_id=328'$params = http_build_query([
'portal_id' => 'vJW4',
'project_id' => 328,
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/project/detail?'.$params);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const params = new URLSearchParams({ portal_id: 'vJW4', project_id: '328' });
const res = await fetch(`https://ahsuite.com/api/portal/project/detail?${params}`, {
headers: { Authorization: `Basic ${auth}` },
});
console.log(await res.json());import requests
res = requests.get(
'https://ahsuite.com/api/portal/project/detail',
params={'portal_id': 'vJW4', 'project_id': 328},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "",
"data": {
"name": "Website redesign",
"description": "",
"share_with_client": 1,
"share_with_team": 1,
"created_date": "2026-06-02 14:20:11",
"project_id": 328,
"tasks": [
{ "name": "Send the June report", "status": "ready_for_work", "assigned_to": "contact 1 (Client)", "due_date": "2026-12-10 00:00:00", "task_id": 3659 }
],
"phases": [
{ "name": "Phase 1", "phase_id": 91, "tasks": [] },
{ "name": "Phase 2", "phase_id": 92, "tasks": [
{ "name": "child 1", "status": "ready_for_work", "assigned_to": "Steve 1", "due_date": null, "task_id": 3702 }
] }
],
"resources": [
{ "type": "page", "name": "Onboarding", "page_id": "9dRt" },
{ "type": "file", "name": "Signed contract", "file_id": "7pQ1" },
{ "type": "conversation", "name": "Q3 planning", "conversation_id": 276 },
{ "type": "password", "name": "Hosting login", "password_id": "4hLq" }
]
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"project_id\":[\"The project id field is required.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Project record not found."
}Update a Project
Rename a project and/or change its description — the same as editing the name and description on the project screen. This is a partial update: only the parameters you send are changed.
404.p, br, strong, em, ul, ol, li, a. Max 5000 characters. Empty string clears it.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "project_id=328" \
--data-urlencode "name=Website redesign (phase 2)" \
--data-urlencode "description=Homepage, pricing and blog.
" \
'https://ahsuite.com/api/portal/project/update'$ch = curl_init();
$data = [
'portal_id' => 'vJW4',
'project_id' => 328,
'name' => 'Website redesign (phase 2)',
'description' => 'Homepage, pricing and blog.
',
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/project/update');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams({ portal_id: 'vJW4', project_id: '328', name: 'Website redesign (phase 2)', description: 'Homepage, pricing and blog.
' });
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/project/update', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/project/update',
data={'portal_id': 'vJW4', 'project_id': 328, 'name': 'Website redesign (phase 2)', 'description': 'Homepage, pricing and blog.
'},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Project has been updated successfully.",
"data": {
"project_id": 328
}
}{
"success": false,
"message": "Validation Error",
"data": "No settings were sent to update."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Project record not found."
}Add a Project Phase
Add a phase to a project — the same as Add Phase on the project screen. Phases group the project's tasks; a new phase is added at the end. Returns the new phase_id.
Phase N (next number), as in the app.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "project_id=328" \
--data-urlencode "name=Discovery" \
'https://ahsuite.com/api/portal/project/phase/add'$ch = curl_init();
$data = [
'portal_id' => 'vJW4',
'project_id' => 328,
'name' => 'Discovery',
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/project/phase/add');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams({ portal_id: 'vJW4', project_id: '328', name: 'Discovery' });
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/project/phase/add', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/project/phase/add',
data={'portal_id': 'vJW4', 'project_id': 328, 'name': 'Discovery'},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Phase has been added successfully.",
"data": {
"project_id": 328,
"phase_id": 94,
"name": "Discovery"
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"name\":[\"The name may not be greater than 200 characters.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Project record not found."
}Update a Project Phase
Rename a phase of a project — the same as clicking the pencil next to a phase name on the project screen.
Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "project_id=328" \
--data-urlencode "phase_id=94" \
--data-urlencode "name=Discovery & research" \
'https://ahsuite.com/api/portal/project/phase/update'$ch = curl_init();
$data = [
'portal_id' => 'vJW4',
'project_id' => 328,
'phase_id' => 94,
'name' => 'Discovery & research',
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/project/phase/update');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams({ portal_id: 'vJW4', project_id: '328', phase_id: '94', name: 'Discovery & research' });
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/project/phase/update', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/project/phase/update',
data={'portal_id': 'vJW4', 'project_id': 328, 'phase_id': 94, 'name': 'Discovery & research'},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Phase has been updated successfully.",
"data": {
"project_id": 328,
"phase_id": 94,
"name": "Discovery & research"
}
}{
"success": false,
"message": "Validation Error",
"data": "Phase name is blank."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Phase record not found on this project."
}Add Tasks to a Project
Put existing tasks of the portal into a project — the same as Select tasks on the project screen — optionally into a specific phase. Also use it to move a task that is already in the project to another phase. To create a brand-new task inside a project, create the task first and then add it here.
task_ids[] entry (from List Tasks). Up to 20 per request. A task of another portal, or one that is archived or canceled, returns 404 and nothing is changed.phases[].phase_id. Omit to use the project's last phase (as the app does); send 0 for no phase (top of the project).Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "project_id=328" \
--data-urlencode "task_ids[]=3659" \
--data-urlencode "task_ids[]=3702" \
--data-urlencode "phase_id=92" \
'https://ahsuite.com/api/portal/project/task/add'$ch = curl_init();
$data = [
'portal_id' => 'vJW4',
'project_id' => 328,
'task_ids' => [3659, 3702],
'phase_id' => 92,
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/project/task/add');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams();
body.append('portal_id', 'vJW4');
body.append('project_id', '328');
body.append('task_ids[]', '3659');
body.append('task_ids[]', '3702');
body.append('phase_id', '92');
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/project/task/add', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/project/task/add',
data={'portal_id': 'vJW4', 'project_id': 328, 'task_ids[]': [3659, 3702], 'phase_id': 92},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Tasks have been added to the project.",
"data": {
"project_id": 328,
"phase_id": 92,
"task_ids": [3659, 3702]
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"task_ids\":[\"The task ids field is required.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Task record not found: 3999"
}Add Resources to a Project
Link existing pages, files, conversations or passwords of the portal to a project — the same as Add Resources on the project screen. Resources that are already on the project are skipped. Every resource is checked against the portal first; if any one is not found, nothing is linked.
resources[0][type] and resources[0][id].page, file, conversation, password.page_id from List Pages, file_id from List Files, conversation_id from List Conversations, password_id from List Passwords. A resource of another portal or an archived one returns 404.Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=vJW4" \
--data-urlencode "project_id=328" \
--data-urlencode "resources[0][type]=page" \
--data-urlencode "resources[0][id]=l78w" \
--data-urlencode "resources[1][type]=file" \
--data-urlencode "resources[1][id]=ZrV1" \
--data-urlencode "resources[2][type]=conversation" \
--data-urlencode "resources[2][id]=276" \
'https://ahsuite.com/api/portal/project/resource/add'$ch = curl_init();
$data = [
'portal_id' => 'vJW4',
'project_id' => 328,
'resources' => [
['type' => 'page', 'id' => 'l78w'],
['type' => 'file', 'id' => 'ZrV1'],
['type' => 'conversation', 'id' => '276'],
],
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/project/resource/add');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams();
body.append('portal_id', 'vJW4');
body.append('project_id', '328');
body.append('resources[0][type]', 'page');
body.append('resources[0][id]', 'l78w');
body.append('resources[1][type]', 'file');
body.append('resources[1][id]', 'ZrV1');
body.append('resources[2][type]', 'conversation');
body.append('resources[2][id]', '276');
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/project/resource/add', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/project/resource/add',
data={
'portal_id': 'vJW4', 'project_id': 328,
'resources[0][type]': 'page', 'resources[0][id]': 'l78w',
'resources[1][type]': 'file', 'resources[1][id]': 'ZrV1',
'resources[2][type]': 'conversation', 'resources[2][id]': '276',
},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Resources have been added to the project.",
"data": {
"project_id": 328,
"added": 3
}
}{
"success": false,
"message": "Validation Error",
"data": "These resources are already linked to the project — nothing to change."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "resources[1]: file ZrV9 not found in this portal."
}Create a Contact
Add a client user (contact) to an existing portal. Requires a valid portal_id and email. The contact is created in a pending state until they accept the invitation. Returns the new contact_id on success.
1 to send the contact an invitation email with a link to set their password. Default 0.1 to enable this user to add or remove other client users to the portal. Optional, default 0.1 to enable this user to receive notifications and updates by email. Optional, default 0.1 to enable this user to manage portal content. Optional, default 0.These permissions apply to this contact only. To set the defaults every new client user on the portal starts with, see Update Client User Settings.
A portal can hold a maximum of 50 contacts.
Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=PORTAL_ID" \
--data-urlencode "email=contact@example.com" \
--data-urlencode "name=Contact Name" \
--data-urlencode "default_permissions[email_receive]=1" \
--data-urlencode "default_permissions[allow_page_add]=1" \
--data-urlencode "send_client_email_notify=1" \
'https://ahsuite.com/api/portal/contact/create'$ch = curl_init();
$data = [
'portal_id' => 'PORTAL_ID',
'email' => 'contact@example.com',
'name' => 'Contact Name',
'default_permissions[email_receive]' => 1,
'default_permissions[allow_page_add]' => 1,
'send_client_email_notify' => 1,
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/contact/create');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams({
portal_id: 'PORTAL_ID',
email: 'contact@example.com',
name: 'Contact Name',
'default_permissions[email_receive]': '1',
'default_permissions[allow_page_add]': '1',
send_client_email_notify: '1',
});
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/contact/create', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/contact/create',
auth=('EMAIL', 'TOKEN'),
data={
'portal_id': 'PORTAL_ID',
'email': 'contact@example.com',
'name': 'Contact Name',
'default_permissions[email_receive]': 1,
'default_permissions[allow_page_add]': 1,
'send_client_email_notify': 1,
},
)
print(res.json()){
"success": true,
"message": "Contact has been added successfully.",
"data": {
"contact_id": "vJW4"
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"email\":[\"The email field is required.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Portal record not found."
}List Contacts
Return the client users (contacts) of a portal — the same list as the Client Users block in the app. The primary contact comes first. Each row includes the invite status (invited, active or declined), the contact's permissions. Invited and declined contacts return only their email and status (blank name, permissions = null), like the app. Results are paginated.
1.20, maximum 100.Other unsuccessful response codes: 403, 429, 500.
curl --request GET \
-u "EMAIL:TOKEN" \
'https://ahsuite.com/api/portal/contact/list?portal_id=vJW4&page=1&limit=20'$params = http_build_query([
'portal_id' => 'vJW4',
'page' => 1,
'limit' => 20,
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/contact/list?'.$params);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const params = new URLSearchParams({ portal_id: 'vJW4', page: 1, limit: 20 });
const res = await fetch(`https://ahsuite.com/api/portal/contact/list?${params}`, {
headers: { Authorization: `Basic ${auth}` },
});
console.log(await res.json());import requests
res = requests.get(
'https://ahsuite.com/api/portal/contact/list',
params={'portal_id': 'vJW4', 'page': 1, 'limit': 20},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "",
"data": {
"contacts": [
{
"name": "Jane Cooper",
"email": "jane@example.com",
"primary": 1,
"status": "active",
"permissions": {
"allow_add_portal_users": 1,
"email_receive": 1,
"allow_page_add": 0
},
"added_date": "2026-05-02 10:12:44",
"contact_id": "3Kp9"
},
{
"name": "",
"email": "sam@example.com",
"primary": 0,
"status": "invited",
"permissions": null,
"added_date": "2026-07-19 16:03:21",
"contact_id": "8Qw2"
}
],
"pagination": {
"total": 2,
"page": 1,
"limit": 20,
"total_pages": 1
}
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"portal_id\":[\"The portal id field is required.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Portal record not found."
}Update Portal Preferences
Update the Preferences settings of an existing portal — the same options you see at the top of the portal settings screen. This is a partial update: only the parameters you send are changed, every other portal setting is left untouched.
America/Phoenix, Asia/Kolkata or Europe/London. Fetch the full list →en, es, de, pt, ja, fr, ar, cs, nl, et, fi, el, hi, hu, id, it, ko, no, pl, ro, ru, zh-Hans, sv, th, zh-Hant, tr, vi.align_left or align_center.1 to require 2 step authentication for this portal. Ignored on HIPAA portals, where 2 step authentication always stays on.1 to enable portal search, 0 to disable it.1 to allow file access without login, 0 to require login.1 to hide the large file preview, 0 to show it.Send at least one setting besides portal_id, otherwise the request is rejected.
Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=PORTAL_ID" \
--data-urlencode "timezone=America/Phoenix" \
--data-urlencode "language=en" \
--data-urlencode "portal_search=1" \
'https://ahsuite.com/api/portal/settings/preferences/update'$ch = curl_init();
$data = [
'portal_id' => 'PORTAL_ID',
'timezone' => 'America/Phoenix',
'language' => 'en',
'portal_search' => 1,
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/settings/preferences/update');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams({
portal_id: 'PORTAL_ID',
timezone: 'America/Phoenix',
language: 'en',
portal_search: '1',
});
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/settings/preferences/update', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/settings/preferences/update',
auth=('EMAIL', 'TOKEN'),
data={
'portal_id': 'PORTAL_ID',
'timezone': 'America/Phoenix',
'language': 'en',
'portal_search': 1,
},
)
print(res.json()){
"success": true,
"message": "Portal preferences have been updated successfully.",
"data": {
"portal_id": "vJW4"
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"timezone\":[\"The timezone must be a valid zone.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Portal record not found."
}Update Sidebar Menu
Control which tools appear in a portal's sidebar, what they are called, the order they sit in, and any custom tabs. This is a partial update: only the parameters you send are changed, every other portal setting is left untouched.
1 to show the Pages tab, 0 to hide it.1 to show the File Manager tab, 0 to hide it.1 to show the Password Manager tab, 0 to hide it.1 to show the To Do tab, 0 to hide it.1 to show the Conversations tab, 0 to hide it.1 to show the Projects tab, 0 to hide it.links[0][url] is sent. Sent tabs are added to the existing list; a tab with the same name is updated instead of duplicated. Existing tabs are never removed by the API (remove them from the portal settings page). Max 20 tabs per portal.links[0][name] is sent.1 to open the link in the same tab. Default 0, which opens a new tab.pages, tasks, conversations, files, password, projects. Tools you leave out keep their current position after the ones you send. The portal opens on the first visible tool.Unlike the other parameters, sending links replaces every custom tab on the portal — send the full set you want to keep. Leave links out entirely and the existing custom tabs are untouched. A maximum of 20 custom tabs is allowed.
Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=PORTAL_ID" \
--data-urlencode "show_pages=1" \
--data-urlencode "show_password=0" \
--data-urlencode "label_tasks=Action Items" \
--data-urlencode "links[0][name]=Our Website" \
--data-urlencode "links[0][url]=https://example.com" \
'https://ahsuite.com/api/portal/settings/sidebar/update'$ch = curl_init();
$data = [
'portal_id' => 'PORTAL_ID',
'show_pages' => 1,
'show_password' => 0,
'label_tasks' => 'Action Items',
'links' => [
['name' => 'Our Website', 'url' => 'https://example.com', 'same' => 0],
],
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/settings/sidebar/update');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams({
portal_id: 'PORTAL_ID',
show_pages: '1',
show_password: '0',
label_tasks: 'Action Items',
'links[0][name]': 'Our Website',
'links[0][url]': 'https://example.com',
});
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/settings/sidebar/update', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/settings/sidebar/update',
auth=('EMAIL', 'TOKEN'),
data={
'portal_id': 'PORTAL_ID',
'show_pages': 1,
'show_password': 0,
'label_tasks': 'Action Items',
'links[0][name]': 'Our Website',
'links[0][url]': 'https://example.com',
},
)
print(res.json()){
"success": true,
"message": "Portal sidebar menu has been updated successfully.",
"data": {
"portal_id": "vJW4"
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"show_pages\":[\"The selected show pages is invalid.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Portal record not found."
}Update Account Manager Details
Control the account manager card shown on the portal dashboard — which details appear, what they say, and the action buttons beneath them. This is a partial update: only the parameters you send are changed, every other portal setting is left untouched.
agency_logo or profile_picture.1 to show the account manager card at all, 0 to hide it. When hidden, the settings below have no visible effect.1 to show the avatar, 0 to hide it.1 to show the name, 0 to hide it.1 to show the job title, 0 to hide it.1 to show the email address, 0 to hide it.1 to show the phone number, 0 to hide it.Every parameter in this group may be sent empty to clear it. A cleared value falls back to the account manager's own details.
1 to enable the request button, 0 to disable it.1 to enable the schedule button, 0 to disable it.1 to open the calendar link inside the portal, 0 to open it in a new tab.1 to enable the send a message button, 0 to disable it. This button needs the Conversations tab to be present in the portal sidebar.request, schedule or message.Send at least one setting besides portal_id, otherwise the request is rejected.
Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=PORTAL_ID" \
--data-urlencode "show_card=1" \
--data-urlencode "show_phone=0" \
--data-urlencode "display_name=Priya Sharma" \
--data-urlencode "title=Account Executive" \
--data-urlencode "action_buttons[schedule][enabled]=1" \
--data-urlencode "action_buttons[schedule][text]=Schedule a Call" \
--data-urlencode "action_buttons[schedule][calendar_link]=https://calendly.com/priya" \
--data-urlencode "action_buttons[primary]=schedule" \
'https://ahsuite.com/api/portal/settings/account-manager/update'$ch = curl_init();
$data = [
'portal_id' => 'PORTAL_ID',
'show_card' => 1,
'show_phone' => 0,
'display_name' => 'Priya Sharma',
'title' => 'Account Executive',
'action_buttons' => [
'schedule' => [
'enabled' => 1,
'text' => 'Schedule a Call',
'calendar_link' => 'https://calendly.com/priya',
],
'primary' => 'schedule',
],
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/settings/account-manager/update');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams({
portal_id: 'PORTAL_ID',
show_card: '1',
show_phone: '0',
display_name: 'Priya Sharma',
title: 'Account Executive',
'action_buttons[schedule][enabled]': '1',
'action_buttons[schedule][text]': 'Schedule a Call',
'action_buttons[schedule][calendar_link]': 'https://calendly.com/priya',
'action_buttons[primary]': 'schedule',
});
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/settings/account-manager/update', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/settings/account-manager/update',
auth=('EMAIL', 'TOKEN'),
data={
'portal_id': 'PORTAL_ID',
'show_card': 1,
'show_phone': 0,
'display_name': 'Priya Sharma',
'title': 'Account Executive',
'action_buttons[schedule][enabled]': 1,
'action_buttons[schedule][text]': 'Schedule a Call',
'action_buttons[schedule][calendar_link]': 'https://calendly.com/priya',
'action_buttons[primary]': 'schedule',
},
)
print(res.json()){
"success": true,
"message": "Portal account manager details have been updated successfully.",
"data": {
"portal_id": "vJW4"
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"action_buttons.primary\":[\"The selected action buttons.primary is invalid.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Portal record not found."
}Update Client User Settings
Set the default access and notification preferences that new client users get when they are added to this portal. Existing client users are not changed — to set permissions for one contact, use Create Contact. This is a partial update: only the parameters you send are changed.
1 to enable client users to add or remove other client users to the portal, 0 to disable it.1 to enable client users to receive notifications and updates by email, 0 to disable it.1 to enable client users to manage portal content, 0 to disable it.Send at least one setting besides portal_id, otherwise the request is rejected.
Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=PORTAL_ID" \
--data-urlencode "email_receive=1" \
--data-urlencode "allow_page_add=0" \
'https://ahsuite.com/api/portal/settings/client-user/update'$ch = curl_init();
$data = [
'portal_id' => 'PORTAL_ID',
'email_receive' => 1,
'allow_page_add' => 0,
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/settings/client-user/update');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams({
portal_id: 'PORTAL_ID',
email_receive: '1',
allow_page_add: '0',
});
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/settings/client-user/update', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/settings/client-user/update',
auth=('EMAIL', 'TOKEN'),
data={
'portal_id': 'PORTAL_ID',
'email_receive': 1,
'allow_page_add': 0,
},
)
print(res.json()){
"success": true,
"message": "Portal client user settings have been updated successfully.",
"data": {
"portal_id": "vJW4"
}
}{
"success": false,
"message": "Validation Error",
"data": "No settings were sent to update."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Portal record not found."
}Update Portal Branding
Update the portal's branding — logo, custom CSS and website. This is a partial update: only the parameters you send are changed, everything else keeps its current value.
jpg, png, jpeg, gif, svg, webp. Max size 5 MB.<style> tags. Send an empty value to clear it. Max 50000 characters.Send at least one setting besides portal_id, otherwise the request is rejected.
Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
-F "portal_id=PORTAL_ID" \
-F "logo=@/path/to/logo.png" \
-F "custom_css=.portal-header { background: #1a1a2e; }" \
-F "website=https://client-website.com" \
'https://ahsuite.com/api/portal/settings/branding/update'$ch = curl_init();
$data = [
'portal_id' => 'PORTAL_ID',
'logo' => new CURLFile('/path/to/logo.png'),
'custom_css' => '.portal-header { background: #1a1a2e; }',
'website' => 'https://client-website.com',
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/settings/branding/update');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
import FormData from 'form-data';
import fs from 'fs';
const form = new FormData();
form.append('portal_id', 'PORTAL_ID');
form.append('logo', fs.createReadStream('/path/to/logo.png'));
form.append('custom_css', '.portal-header { background: #1a1a2e; }');
form.append('website', 'https://client-website.com');
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/settings/branding/update', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body: form,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/settings/branding/update',
auth=('EMAIL', 'TOKEN'),
data={
'portal_id': 'PORTAL_ID',
'custom_css': '.portal-header { background: #1a1a2e; }',
'website': 'https://client-website.com',
},
files={'logo': open('/path/to/logo.png', 'rb')},
)
print(res.json()){
"success": true,
"message": "Portal branding has been updated successfully.",
"data": {
"portal_id": "vJW4"
}
}{
"success": false,
"message": "Validation Error",
"data": "No settings were sent to update."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Portal record not found."
}Update Email Notifications
Set the summary email frequency and customize the emails the portal sends to client users. This is a partial update: only the parameters you send are changed. Sending an empty value clears the custom text, and the default template is used again.
off, daily, weekly (each Monday) or monthly (on the 1st).Send at least one setting besides portal_id, otherwise the request is rejected.
Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=PORTAL_ID" \
--data-urlencode "summary_email_frequency=weekly" \
--data-urlencode "emails[invitation][subject]=You are invited to your client portal" \
--data-urlencode "emails[invitation][body]=Hi, your portal is ready. Click below to log in." \
--data-urlencode "emails[needs_attention][body][task_heading]=Tasks waiting for you" \
'https://ahsuite.com/api/portal/settings/email-notifications/update'$ch = curl_init();
$data = [
'portal_id' => 'PORTAL_ID',
'summary_email_frequency' => 'weekly',
'emails' => [
'invitation' => [
'subject' => 'You are invited to your client portal',
'body' => 'Hi, your portal is ready. Click below to log in.',
],
'needs_attention' => [
'body' => ['task_heading' => 'Tasks waiting for you'],
],
],
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/settings/email-notifications/update');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams({
portal_id: 'PORTAL_ID',
summary_email_frequency: 'weekly',
'emails[invitation][subject]': 'You are invited to your client portal',
'emails[invitation][body]': 'Hi, your portal is ready. Click below to log in.',
'emails[needs_attention][body][task_heading]': 'Tasks waiting for you',
});
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/settings/email-notifications/update', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/settings/email-notifications/update',
auth=('EMAIL', 'TOKEN'),
data={
'portal_id': 'PORTAL_ID',
'summary_email_frequency': 'weekly',
'emails[invitation][subject]': 'You are invited to your client portal',
'emails[invitation][body]': 'Hi, your portal is ready. Click below to log in.',
'emails[needs_attention][body][task_heading]': 'Tasks waiting for you',
},
)
print(res.json()){
"success": true,
"message": "Portal email notification settings have been updated successfully.",
"data": {
"portal_id": "vJW4"
}
}{
"success": false,
"message": "Validation Error",
"data": "No settings were sent to update."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Portal record not found."
}Update Custom Variables
Add or update the portal's custom variables. A variable is identified by its name and scope together — sending an existing combination updates that variable's value, a new combination creates it. Variables can also be created when a portal is created (Add Portal) or through page questions (Create Page).
address. Together with scope it identifies the variable. Max 255 characters.portal (default) or user. A portal variable holds one value for the whole portal; a user variable can hold a different value per client user.portal scope — a user variable holds its values per client user, so sending a value with user scope is rejected. Send an empty value to clear it. Max 500 characters.Send at least one variable besides portal_id, otherwise the request is rejected.
Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "portal_id=PORTAL_ID" \
--data-urlencode "variables[0][name]=address" \
--data-urlencode "variables[0][scope]=portal" \
--data-urlencode "variables[0][value]=221B Baker Street" \
--data-urlencode "variables[1][name]=phone" \
--data-urlencode "variables[1][scope]=user" \
'https://ahsuite.com/api/portal/settings/variables/update'$ch = curl_init();
$data = [
'portal_id' => 'PORTAL_ID',
'variables' => [
['name' => 'address', 'scope' => 'portal', 'value' => '221B Baker Street'],
['name' => 'phone', 'scope' => 'user'],
],
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/portal/settings/variables/update');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams({
portal_id: 'PORTAL_ID',
'variables[0][name]': 'address',
'variables[0][scope]': 'portal',
'variables[0][value]': '221B Baker Street',
'variables[1][name]': 'phone',
'variables[1][scope]': 'user',
});
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/portal/settings/variables/update', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/portal/settings/variables/update',
auth=('EMAIL', 'TOKEN'),
data={
'portal_id': 'PORTAL_ID',
'variables[0][name]': 'address',
'variables[0][scope]': 'portal',
'variables[0][value]': '221B Baker Street',
'variables[1][name]': 'phone',
'variables[1][scope]': 'user',
},
)
print(res.json()){
"success": true,
"message": "Portal variables have been updated successfully.",
"data": {
"portal_id": "vJW4"
}
}{
"success": false,
"message": "Validation Error",
"data": "No settings were sent to update."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Portal record not found."
}List Notifications
Return your own notifications, newest first — the same list as the Notifications page in the app (task assigned or done, mentions, file approved, page answered, invoice paid and so on). Notifications belong to the API user, not to one portal, so this endpoint takes no portal_id. Send unread_only=1 to get only the unread ones. When a notification belongs to a portal you own, the row also carries the portal_id and the ID of the related task, file, page, conversation or journal, so you can open it with the detail endpoints.
1 returns only unread notifications. 0 or omitted returns all, read and unread.1. Each page holds 20 notifications.Other unsuccessful response codes: 403, 429, 500.
curl --request GET \
-u "EMAIL:TOKEN" \
'https://ahsuite.com/api/notifications?unread_only=1&page=1'$params = http_build_query([
'unread_only' => 1,
'page' => 1,
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/notifications?'.$params);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const params = new URLSearchParams({ unread_only: '1', page: '1' });
const res = await fetch(`https://ahsuite.com/api/notifications?${params}`, {
headers: { Authorization: `Basic ${auth}` },
});
console.log(await res.json());import requests
res = requests.get(
'https://ahsuite.com/api/notifications',
params={'unread_only': 1, 'page': 1},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "",
"data": {
"notifications": [
{
"text": "You were assigned Send the June report",
"is_read": 0,
"portal": "Acme Inc",
"portal_id": "vJW4",
"created_date": "2026-06-10 09:15:02",
"notification_id": 130,
"task_id": 3659
},
{
"text": "Signed contract was approved by Sam Lee (Client)",
"is_read": 0,
"portal": "Acme Inc",
"portal_id": "vJW4",
"created_date": "2026-06-09 17:40:11",
"notification_id": 129,
"file_id": "ZrV1"
}
],
"unread_count": 12,
"pagination": {
"total": 12,
"page": 1,
"limit": 20,
"total_pages": 1
}
}
}{
"success": false,
"message": "Validation Error",
"data": "{\"page\":[\"The page must be at least 1.\"]}"
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}Mark Notifications Read
Mark your notifications as read, the same as opening a notification or using Mark all read in the app. Send either notification_ids[] for specific notifications or all=1 for every unread one, not both. Only your own notifications can be changed. The response returns how many were marked and your new unread count.
notification_id per entry (from List Notifications), up to 50. An ID that is not yours returns 404 and nothing is changed.1 marks every unread notification as read. Do not send it together with notification_ids[].Other unsuccessful response codes: 403, 429, 500.
curl --request POST \
-u "EMAIL:TOKEN" \
--data-urlencode "notification_ids[]=130" \
--data-urlencode "notification_ids[]=129" \
'https://ahsuite.com/api/notifications/read'$ch = curl_init();
$data = [
'notification_ids' => [130, 129],
];
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/notifications/read');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const body = new URLSearchParams();
body.append('notification_ids[]', '130');
body.append('notification_ids[]', '129');
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/notifications/read', {
method: 'POST',
headers: { Authorization: `Basic ${auth}` },
body,
});
console.log(await res.json());import requests
res = requests.post(
'https://ahsuite.com/api/notifications/read',
data={'notification_ids[]': [130, 129]},
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "Notifications have been marked as read.",
"data": {
"marked_read": 2,
"unread_count": 10
}
}{
"success": false,
"message": "Validation Error",
"data": "Send exactly one of: notification_ids or all=1."
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}{
"success": false,
"message": "Not Found",
"data": "Notification record not found: 999"
}List Timezones
Return every timezone identifier accepted by the timezone parameter. Use this to build a picker or to validate a value before saving it — anything in this list is guaranteed to be accepted.
None. This endpoint takes no parameters.
Other unsuccessful response codes: 403, 429, 500.
curl --request GET \
-u "EMAIL:TOKEN" \
'https://ahsuite.com/api/timezones'$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://ahsuite.com/api/timezones');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
$result = curl_exec($ch);import fetch from 'node-fetch';
const auth = Buffer.from('EMAIL:TOKEN').toString('base64');
const res = await fetch('https://ahsuite.com/api/timezones', {
headers: { Authorization: `Basic ${auth}` },
});
console.log(await res.json());import requests
res = requests.get(
'https://ahsuite.com/api/timezones',
auth=('EMAIL', 'TOKEN'),
)
print(res.json()){
"success": true,
"message": "",
"data": [
"Africa/Cairo",
"America/Phoenix",
"Asia/Kolkata",
"Europe/London",
"..."
]
}{
"success": false,
"message": "Invalid owner email and authorization_token."
}