Skip to content

PWA ​

Methods for PWA apps: find the ones you need, create a new one, change settings, turn it on or off, clone it and archive it. For what the settings themselves mean, see the "PWA - creating (General tab)" section and the neighbouring guide pages.

MethodAddressWhat it does
GET/pwasList PWAs
GET/pwas/{id}One PWA with all settings
POST/pwasCreate a PWA
PUT/pwas/{id}Update, turn on or off
POST/pwas/{id}/cloneClone
DELETE/pwas/{id}Archive

Replace YOUR_KEY and panel.example.com with your own.

List PWAs ​

GET/pwas

PWAs of your teams, new and recently changed first. Archived ones are not shown.

ParameterWhat it filters
searchPart of the name
domainPart of a domain attached to the PWA
statusactive - only enabled, inactive - only disabled
archived1 - only archived
team_idTeam
offer_countryCountry in the offer links, two-letter code (US, DE); PWAs with an ANY link match too
group_idAccess group
cloudflare_id, filter_account_idCloudflare or filter account
page, per_pagePage and its size (100 by default)
bash
curl "https://panel.example.com/api/pwas?status=active&offer_country=US" \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Accept: application/json"
php
<?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('pwas', ['query' => ['status' => 'active', 'offer_country' => 'US']]);
$pwas = json_decode($response->getBody(), true)['data']['pwas'];

foreach ($pwas as $pwa) {
    echo "{$pwa['id']} {$pwa['name']}: {$pwa['stat_installs']} installs" . PHP_EOL;
}
python
# pip install requests
import requests

API = "https://panel.example.com/api"
HEADERS = {"Authorization": "Bearer YOUR_KEY", "Accept": "application/json"}

response = requests.get(f"{API}/pwas", headers=HEADERS, params={"status": "active", "offer_country": "US"}, timeout=30)
pwas = response.json()["data"]["pwas"]

for pwa in pwas:
    print(pwa["id"], pwa["name"], pwa["stat_installs"], "installs")
js
// 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?status=active&offer_country=US`, { headers, signal: AbortSignal.timeout(30000) });
const { data } = await response.json();

for (const pwa of data.pwas) {
  console.log(pwa.id, pwa.name, pwa.stat_installs, 'installs');
}
Response
json
{
  "data": {
    "meta": { "current_page": 1, "per_page": 100, "last_page": 1, "total": 1 },
    "pwas": [
      {
        "id": "WZAX",
        "name": "Promo EN",
        "active": true,
        "archived": false,
        "filter_stream_id": null,
        "icon_url": "/api/pwas/WZAX/assets/icon-96.png",
        "stat_uniques": 1520,
        "stat_installs": 410,
        "stat_registrations": 96,
        "stat_deposits": 21,
        "stat_repeat_deposits": 7,
        "similar_count": 0,
        "team": { "id": "WQby", "name": "Team A" },
        "user": { "id": "WZAX", "name": "Buyer", "email": "[email protected]" },
        "domains": [ { "id": "Xk9P", "domain": "fit-tracker.example" } ],
        "groups": [],
        "created_at": "05 Oct 2026 14:03",
        "updated_at": "05 Oct 2026 14:03"
      }
    ]
  }
}

The stat_* counters are all-time: unique visitors, installs, registrations, deposits and repeat deposits. For statistics over a period use the "Statistics" section.

Errors
CodemessageWhen
422The offer country field format is invalid.offer_country is not two Latin letters

One PWA ​

GET/pwas/{id}

Everything about the app, including the settings in the config field.

bash
curl https://panel.example.com/api/pwas/WZAX \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Accept: application/json"
php
<?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('pwas/WZAX');
$pwa = json_decode($response->getBody(), true)['data']['pwa'];

echo $pwa['name'] . ': ' . ($pwa['active'] ? 'enabled' : 'disabled') . PHP_EOL;
python
# pip install requests
import requests

API = "https://panel.example.com/api"
HEADERS = {"Authorization": "Bearer YOUR_KEY", "Accept": "application/json"}

response = requests.get(f"{API}/pwas/WZAX", headers=HEADERS, timeout=30)
pwa = response.json()["data"]["pwa"]

print(pwa["name"], "enabled" if pwa["active"] else "disabled")
js
// 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/WZAX`, { headers, signal: AbortSignal.timeout(30000) });
const { data } = await response.json();

console.log(data.pwa.name, data.pwa.active ? 'enabled' : 'disabled');
Response (shortened)
json
{
  "data": {
    "pwa": {
      "id": "WZAX",
      "name": "Promo EN",
      "active": true,
      "cloudflare_id": "WZAX",
      "filter_account_id": null,
      "filter_stream_id": null,
      "domains": [ { "id": "Xk9P", "domain": "fit-tracker.example" } ],
      "config": {
        "general": { "name": "Fit Tracker", "language": "en", "templateAndroid": "google-v1" },
        "offerLinks": {
          "US": [ { "lang": "en", "link": "https://offer.example/?sub1={sub1}" } ]
        },
        "metrics": { "facebook": { "pixelId": null, "events": [] }, "google": { "tagId": null } },
        "comments": [],
        "translations": []
      },
      "similar": []
    }
  }
}

The config fields mirror the tabs of the PWA form: general - "General", offerLinks - offer links by country, metrics - pixels, comments - reviews, translations - translations.

In offerLinks the country ANY is the fallback: its link goes to a visitor with no link in their own country (the country is not listed, not detected, or has no link for their language); the language * means any. Updating a PWA with config needs at least one link, and with general.white.type = off - a link under ANY.

Errors
CodemessageWhen
403This action is unauthorized.No access to the PWA: it is not in your teams (for a customer - not created by them) and not shared with you through an access group
404Not foundNo PWA with this id

Creating a PWA ​

POST/pwas

FieldRequiredWhat it is
nameyesPWA name in the panel
cloudflare_idyesCloudflare account, list - GET/cloudflares
team_idfor team_lead and adminTeam: the id from GET/teams (the "References" section) or team.id of any GET/pwas row; for a customer their own team is used, the value passed is ignored
activenotrue - turn on right away. By default a PWA is created disabled
filter_account_id, filter_stream_idnoFilter account and a stream in it
confignoSettings, as in the GET/pwas/{id} response
bash
curl -X POST https://panel.example.com/api/pwas \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Promo EN",
    "team_id": "WQby",
    "cloudflare_id": "WZAX",
    "config": {
      "general": { "name": "Fit Tracker", "language": "en", "templateAndroid": "google-v1" },
      "offerLinks": { "US": [ { "lang": "en", "link": "https://offer.example/?sub1={sub1}" } ] }
    }
  }'
php
<?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->post('pwas', ['json' => [
    'name' => 'Promo EN',
    'team_id' => 'WQby',
    'cloudflare_id' => 'WZAX',
    'config' => [
        'general' => ['name' => 'Fit Tracker', 'language' => 'en', 'templateAndroid' => 'google-v1'],
        'offerLinks' => ['US' => [['lang' => 'en', 'link' => 'https://offer.example/?sub1={sub1}']]],
    ],
]]);
$pwa = json_decode($response->getBody(), true)['data']['pwa'];

echo "Created PWA {$pwa['id']}" . PHP_EOL;
python
# 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={
    "name": "Promo EN",
    "team_id": "WQby",
    "cloudflare_id": "WZAX",
    "config": {
        "general": {"name": "Fit Tracker", "language": "en", "templateAndroid": "google-v1"},
        "offerLinks": {"US": [{"lang": "en", "link": "https://offer.example/?sub1={sub1}"}]},
    },
}, timeout=30)
pwa = response.json()["data"]["pwa"]

print("Created PWA", pwa["id"])
js
// 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({
    name: 'Promo EN',
    team_id: 'WQby',
    cloudflare_id: 'WZAX',
    config: {
      general: { name: 'Fit Tracker', language: 'en', templateAndroid: 'google-v1' },
      offerLinks: { US: [{ lang: 'en', link: 'https://offer.example/?sub1={sub1}' }] },
    },
  }),
  signal: AbortSignal.timeout(30000),
});
const { data } = await response.json();

console.log('Created PWA', data.pwa.id);

The response is the created PWA in data.pwa, with active, archived, team, user and an empty domains. A domain is attached to the PWA separately, see the "Domains" section.

Errors
CodemessageWhen
422The name field is required. (and 2 more errors)name, cloudflare_id or team_id is missing - errors says which
422You do not have access to this teamteam_id is a team you are not in
422The cloudflare id field must be an integer.cloudflare_id not found - a wrong id was passed
422The offer links country "USA" must be a two-letter code or ANY.A country in config.offerLinks is neither a two-letter code nor ANY
422The link field format is invalid.An offer link doesn't start with http:// or https://
422The selected template android is invalid.Unknown template in config.general.templateAndroid / templateIos

Cloning is easier

A PWA has a lot of settings, and building config from scratch takes time. It is usually easier to set up one PWA in the panel and make new ones as its clones.

Updating, turning on and off ​

PUT/pwas/{id}

Changes only the fields passed. Accepts the same fields as creation.

team_id is optional on update: without it the PWA stays in its team, and you only send it to move the PWA to another team (a team_lead - to one of their own). A customer can change only their own PWAs and those shared with them through an access group.

Turn a PWA on:

bash
curl -X PUT https://panel.example.com/api/pwas/WZAX \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"active": true}'
php
<?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->put('pwas/WZAX', ['json' => ['active' => true]]);
$pwa = json_decode($response->getBody(), true)['data']['pwa'];

echo $pwa['name'] . ': ' . ($pwa['active'] ? 'enabled' : 'disabled') . PHP_EOL;
python
# pip install requests
import requests

API = "https://panel.example.com/api"
HEADERS = {"Authorization": "Bearer YOUR_KEY", "Accept": "application/json"}

response = requests.put(f"{API}/pwas/WZAX", headers=HEADERS, json={"active": True}, timeout=30)
pwa = response.json()["data"]["pwa"]

print(pwa["name"], "enabled" if pwa["active"] else "disabled")
js
// 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/WZAX`, {
  method: 'PUT',
  headers,
  body: JSON.stringify({ active: true }),
  signal: AbortSignal.timeout(30000),
});
const { data } = await response.json();

console.log(data.pwa.name, data.pwa.active ? 'enabled' : 'disabled');

To turn it off - the same with "active": false. Team members get a notification that the PWA was stopped.

config is replaced as a whole

If the request contains config, it replaces all PWA settings instead of adding to them: everything missing from the request is erased. So first get the current config, change what you need in it and send it back as a whole.

Change the offer link for the US:

bash
# 1. Save the current PWA
curl https://panel.example.com/api/pwas/WZAX \
  -H "Authorization: Bearer YOUR_KEY" -H "Accept: application/json" > pwa.json

# 2. Change the link and send config as a whole (needs jq)
jq '{config: (.data.pwa.config
     | .offerLinks.US = [{"lang": "en", "link": "https://new-offer.example/?sub1={sub1}"}])}' pwa.json \
| curl -X PUT https://panel.example.com/api/pwas/WZAX \
    -H "Authorization: Bearer YOUR_KEY" -H "Accept: application/json" \
    -H "Content-Type: application/json" -d @-
php
<?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,
]);

// 1. The current PWA
$pwa = json_decode($api->get('pwas/WZAX')->getBody(), true)['data']['pwa'];

// 2. Change the US link, the rest of config stays as it was
$config = $pwa['config'];
$config['offerLinks']['US'] = [['lang' => 'en', 'link' => 'https://new-offer.example/?sub1={sub1}']];

// 3. Send config as a whole
$response = $api->put('pwas/WZAX', ['json' => ['config' => $config]]);
$updated = json_decode($response->getBody(), true)['data']['pwa'];

echo $updated['config']['offerLinks']['US'][0]['link'] . PHP_EOL;
python
# pip install requests
import requests

API = "https://panel.example.com/api"
HEADERS = {"Authorization": "Bearer YOUR_KEY", "Accept": "application/json"}

# 1. The current PWA
pwa = requests.get(f"{API}/pwas/WZAX", headers=HEADERS, timeout=30).json()["data"]["pwa"]

# 2. Change the US link, the rest of config stays as it was
config = pwa["config"]
config["offerLinks"]["US"] = [{"lang": "en", "link": "https://new-offer.example/?sub1={sub1}"}]

# 3. Send config as a whole
response = requests.put(f"{API}/pwas/WZAX", headers=HEADERS, json={"config": config}, timeout=30)
updated = response.json()["data"]["pwa"]

print(updated["config"]["offerLinks"]["US"][0]["link"])
js
// 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' };

// 1. The current PWA
const current = await fetch(`${API}/pwas/WZAX`, { headers, signal: AbortSignal.timeout(30000) });
const { pwa } = (await current.json()).data;

// 2. Change the US link, the rest of config stays as it was
const config = pwa.config;
config.offerLinks.US = [{ lang: 'en', link: 'https://new-offer.example/?sub1={sub1}' }];

// 3. Send config as a whole
const response = await fetch(`${API}/pwas/WZAX`, {
  method: 'PUT',
  headers,
  body: JSON.stringify({ config }),
  signal: AbortSignal.timeout(30000),
});
const { data } = await response.json();

console.log(data.pwa.config.offerLinks.US[0].link);

The response is the PWA after the change in data.pwa.

Errors
CodemessageWhen
422You do not have access to this teamteam_id is a team you are not in
422At least one offer link is required.config.offerLinks has no link
422The white page is off - at least one offer link with country ANY is required.general.white.type is off and there is no link under ANY
403This action is unauthorized.No access to the PWA: it is not in your teams (for a customer - not created by them) and not shared with you through an access group

Errors in config are the same as on creation.

Cloning ​

POST/pwas/{id}/clone

Makes copies of a PWA, one per name in names.

A copy gets the original's settings, images, Cloudflare and filter accounts and groups. Domains and the filter stream are not copied, and the copy itself is created disabled: attach a domain, set the stream and turn it on.

bash
curl -X POST https://panel.example.com/api/pwas/WZAX/clone \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"names": ["Promo EN 2", "Promo EN 3"]}'
php
<?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->post('pwas/WZAX/clone', ['json' => ['names' => ['Promo EN 2', 'Promo EN 3']]]);
$clones = json_decode($response->getBody(), true)['data']['clones'];

foreach ($clones as $clone) {
    echo "{$clone['id']} {$clone['name']}" . PHP_EOL;
}
python
# 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/WZAX/clone", headers=HEADERS, json={"names": ["Promo EN 2", "Promo EN 3"]}, timeout=30)
clones = response.json()["data"]["clones"]

for clone in clones:
    print(clone["id"], clone["name"])
js
// 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/WZAX/clone`, {
  method: 'POST',
  headers,
  body: JSON.stringify({ names: ['Promo EN 2', 'Promo EN 3'] }),
  signal: AbortSignal.timeout(30000),
});
const { data } = await response.json();

for (const clone of data.clones) {
  console.log(clone.id, clone.name);
}
Response
json
{
  "data": {
    "clones": [
      { "id": "WQby", "name": "Promo EN 2", "domains": [] },
      { "id": "Lm3T", "name": "Promo EN 3", "domains": [] }
    ]
  }
}
Errors
CodemessageWhen
422The names field is required.No names, or it is an empty list
403This action is unauthorized.No access to the PWA: it is not in your teams (for a customer - not created by them) and not shared with you through an access group

Archiving ​

DELETE/pwas/{id}

Archives the PWA together with its flows: the app stops opening, but the statistics are kept. Archived PWAs are shown in the list with archived=1.

bash
curl -X DELETE https://panel.example.com/api/pwas/WZAX \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Accept: application/json"
php
<?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->delete('pwas/WZAX');

echo $response->getStatusCode() === 200 ? 'PWA archived' . PHP_EOL : 'Error' . PHP_EOL;
python
# pip install requests
import requests

API = "https://panel.example.com/api"
HEADERS = {"Authorization": "Bearer YOUR_KEY", "Accept": "application/json"}

response = requests.delete(f"{API}/pwas/WZAX", headers=HEADERS, timeout=30)

print("PWA archived" if response.ok else "Error")
js
// 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/WZAX`, { method: 'DELETE', headers, signal: AbortSignal.timeout(30000) });

console.log(response.ok ? 'PWA archived' : 'Error');

Response: {"data": true}.

Errors
CodemessageWhen
403This action is unauthorized.No access to the PWA: it is not in your teams (for a customer - not created by them) and not shared with you through an access group