API introduction
The PWA Hub API is for your scripts: pull statistics and conversions, clone PWAs for a new campaign, attach a domain, turn an app on or off - without manual work in the panel.
The API gives you what you do in the panel, with the same rights: a script sees and changes exactly the PWAs, splits and other resources you do - team_lead the resources of their teams, customer their own, and both - whatever is shared with them through access groups. Settings, users, integration accounts, the password and the key itself are not available through the API - only in the panel.
Postman collection
Every API method, ready to run without code: download the collection, import it into Postman and set the baseUrl (your panel address with /api) and apiKey variables.
Getting a key
- An administrator turns on Manage API Token for you (see the "Users" section). Administrators need it too: without the toggle the key is neither issued nor accepted.
- Open Profile and click Generate Key in the API Key card (see the "Profile" section).
- Copy the key and store it in your script's settings.
The key is a password
With the key, anyone can do everything you can. Don't publish it in repositories, chats or code that runs in a browser. If the key leaks, regenerate it in your profile - the old one stops working immediately.
The API works only if it is included in your license, and only while the license is active.
Request
The API address is your panel's domain with /api at the end, for example https://panel.example.com/api. The key goes in the Authorization header, the request body is JSON:
curl https://panel.example.com/api/user/profile \
-H "Authorization: Bearer YOUR_KEY" \
-H "Accept: application/json"<?php
// composer require guzzlehttp/guzzle
require 'vendor/autoload.php';
$api = new GuzzleHttp\Client([
'base_uri' => 'https://panel.example.com/api/',
'headers' => ['Authorization' => 'Bearer YOUR_KEY', 'Accept' => 'application/json'],
'timeout' => 30,
]);
$response = $api->get('user/profile');
$user = json_decode($response->getBody(), true)['data']['user'];
echo $user['email'] . PHP_EOL;# pip install requests
import requests
API = "https://panel.example.com/api"
HEADERS = {"Authorization": "Bearer YOUR_KEY", "Accept": "application/json"}
response = requests.get(f"{API}/user/profile", headers=HEADERS, timeout=30)
user = response.json()["data"]["user"]
print(user["email"])// Node.js 18+, no dependencies; save as script.mjs
const API = 'https://panel.example.com/api';
const headers = { Authorization: 'Bearer YOUR_KEY', Accept: 'application/json', 'Content-Type': 'application/json' };
const response = await fetch(`${API}/user/profile`, { headers, signal: AbortSignal.timeout(30000) });
const { data } = await response.json();
console.log(data.user.email);This request is a handy key check: it returns the user the key belongs to, and their teams.
Identifiers
Every object has a short string identifier like WZAX - both in responses and in requests (team_id, cloudflare_id and so on). Take them from API responses or from the panel's address bar.
Response
The data is always in the data field:
{
"data": {
"pwa": { "id": "WZAX", "name": "Promo EN", "active": true }
}
}Lists come in pages: data.meta holds the page number and the total count, next to it are the records themselves under the resource name.
{
"data": {
"meta": { "current_page": 1, "per_page": 100, "last_page": 3, "total": 245 },
"pwas": [ { "id": "WZAX", "name": "Promo EN" } ]
}
}The page is set with the page parameter, its size with per_page (100 by default). To fetch everything, walk the pages until current_page reaches last_page.
Dates in responses look like 05 Oct 2026 14:03, in UTC.
Errors
An error is an HTTP code plus JSON with data: null and the text in message:
{
"data": null,
"message": "The name field is required. (and 2 more errors)",
"errors": {
"name": ["The name field is required."],
"cloudflare_id": ["The cloudflare id field is required."]
}
}| Code | When | What to do |
|---|---|---|
401 | Unauthenticated - no Authorization header; Invalid API key - the key is wrong, regenerated, or your Manage API Token was turned off; API key access is not enabled - the API is not in the license; Account is not approved - the account is not approved | Check the key in your profile, contact an administrator |
403 | Not available with an API key - this method is available only in the panel; This action is unauthorized. - no rights to the object; or the license is inactive (then the response has an error field) | Check that the object is in your team |
404 | Not found - there is no object with this identifier | Check the identifier |
422 | Invalid data - errors lists the fields and what is wrong with them | Fix the fields from errors |
429 | Too many requests. Try again in a minute. or Too many invalid API key attempts. Try again in a minute. - limit exceeded, see "Limits" | Wait a minute and retry |
Error handling
Check the response code before reading data. On an error print message, and for 422 also the fields from errors: they say exactly what to fix. The example below deliberately creates a PWA without a name and prints what the server answered.
curl -X POST https://panel.example.com/api/pwas \
-H "Authorization: Bearer YOUR_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{}' \
-w "\nHTTP %{http_code}\n"<?php
// composer require guzzlehttp/guzzle
require 'vendor/autoload.php';
$api = new GuzzleHttp\Client([
'base_uri' => 'https://panel.example.com/api/',
'headers' => ['Authorization' => 'Bearer YOUR_KEY', 'Accept' => 'application/json'],
'timeout' => 30,
'http_errors' => false, // don't throw on 4xx/5xx, we handle the response ourselves
]);
$response = $api->post('pwas', ['json' => []]);
$body = json_decode($response->getBody(), true);
if ($response->getStatusCode() >= 400) {
echo "Error {$response->getStatusCode()}: {$body['message']}" . PHP_EOL;
foreach ($body['errors'] ?? [] as $field => $messages) {
echo " {$field}: " . implode(' ', $messages) . PHP_EOL;
}
exit(1);
}
echo "Created PWA {$body['data']['pwa']['id']}" . PHP_EOL;# pip install requests
import requests
API = "https://panel.example.com/api"
HEADERS = {"Authorization": "Bearer YOUR_KEY", "Accept": "application/json"}
response = requests.post(f"{API}/pwas", headers=HEADERS, json={}, timeout=30)
body = response.json()
if not response.ok:
print(f"Error {response.status_code}: {body['message']}")
for field, messages in body.get("errors", {}).items():
print(f" {field}: {' '.join(messages)}")
raise SystemExit(1)
print("Created PWA", body["data"]["pwa"]["id"])// Node.js 18+, no dependencies; save as script.mjs
const API = 'https://panel.example.com/api';
const headers = { Authorization: 'Bearer YOUR_KEY', Accept: 'application/json', 'Content-Type': 'application/json' };
const response = await fetch(`${API}/pwas`, {
method: 'POST',
headers,
body: JSON.stringify({}),
signal: AbortSignal.timeout(30000),
});
const body = await response.json();
if (!response.ok) {
console.log(`Error ${response.status}: ${body.message}`);
for (const [field, messages] of Object.entries(body.errors ?? {})) {
console.log(` ${field}: ${messages.join(' ')}`);
}
process.exit(1);
}
console.log('Created PWA', body.data.pwa.id);Output:
Error 422: The name field is required. (and 2 more errors)
name: The name field is required.
team_id: The team id field is required.
cloudflare_id: The cloudflare id field is required.Don't retry 4xx in a loop
4xx errors other than 429 don't go away on their own: repeating the same request gives the same answer. Only 429 (after a minute) and network or 5xx errors are worth retrying.
Limits
- 300 requests per minute per user - across all methods together.
- 10 push sends per minute per user (
POST/pwa_pushes/{id}/send) - every send is a real mailing. - 10 key refusals with
401per minute from one IP - a wrong key, an unapproved account or the API missing from the license. After that any request with a key from that IP gets429until the end of the minute counted from the first refusal - even with the correct key. If your script got a401, stop it instead of retrying.
What is available
| Section | What you can do |
|---|---|
| PWA | list, view, create, update, turn on and off, clone, archive |
| Splits | list, view, create, update, turn on and off, clone, archive |
| Flows | list, view, add a PWA to a split, change the weight, remove from a split |
| Domains | list, attach to a PWA or split, toggle and recreate DNS, delete |
| Pushes | list, view, create, update, preview, audience, send, clone, archive |
| Landings | list |
| Pixels and tags | list, view, add, update, archive |
| Statistics | dashboard, funnel, sub tag statistics |
| Conversions | list, view, resend to Facebook |
| Export | conversions and visits as CSV for a period |
| References | teams, Cloudflare accounts, filter accounts and their streams |