Co je to?
Poznámka pro administrátora: Toto je vývojářská příručka pro vytváření „doplňků“ – vlastních rozšíření, která přidávají nové funkce nebo integrace do marketplace vašeho e-shopu. Představte si to jako manuál pro vytváření vlastních „aplikací“ pro platformu.
Pro koho je to určeno? Administrátoři by měli tento dokument předat svému vývojovému týmu nebo externí agentuře. Je vysoce technický a obsahuje pokyny potřebné k vytvoření, připojení a správě vlastního doplňku, který bezproblémově funguje ve vašem e-shopu.
Klíčové výhody pro e-shop:
- Funkce na míru: Vytvořte přesně ty funkce, které vaše firma potřebuje, pokud ještě nejsou dostupné ve standardním marketplace.
- Bezproblémová integrace: Vlastní doplňky vypadají a fungují jako součást platformy, což zajišťuje konzistentní uživatelské prostředí pro vaše zaměstnance.
- Zabezpečená připojení: Systém automaticky zpracovává veškeré komplexní zabezpečení (např. „tokeny“), takže vaše vlastní nástroje mohou bezpečně přistupovat k vašim datům.
- Správa životního cyklu: Snadno instalujte, pozastavujte nebo odstraňujte své vlastní nástroje stejně jako jakoukoli jinou aplikaci v marketplace.
API pro vývojáře doplňků
Tato příručka vysvětluje, jak vytvořit doplněk pro marketplace {productName}. Váš doplněk je v systému registrován se sadou URL adres, které platforma volá v konkrétních momentech životního cyklu — aktivace, deaktivace, pozastavení/obnovení — a URL adres, které může uživatel otevřít pro konfiguraci a další zobrazení.
Přehled
Záznam verze doplňku obsahuje následující pole URL. Každé pole je volitelné; uveďte pouze ta, která váš doplněk vyžaduje.
| Pole | Kdy je voláno | Metoda |
|---|---|---|
activationUrl |
Uživatel nainstaluje doplněk | POST |
deactivationUrl |
Uživatel odinstaluje doplněk | POST |
pauseUnpauseUrl |
Uživatel pozastaví nebo obnoví doplněk | POST |
configUrl |
Uživatel klikne na „Konfigurovat“ | Otevřeno v prohlížeči (iframe nebo odkaz) |
statusUrl |
Platforma dotazuje aktuální stav doplňku | GET |
Kromě toho pole urls obsahuje další odkazy, které se zobrazují v postranním panelu detailu doplňku (např. protokoly, reporty). Viz sekce Seznam URL adres níže.
Verze doplňku
Doplněk marketplace má dvě úrovně. Samotný doplněk je pouze jeho identita: kód, název, kategorie, autor, stav a ukazatel na jeho aktuální verzi. Vše, co popisuje, co doplněk je a jak s ním platforma zachází, se nachází ve verzi doplňku: texty, obrázky, cena, všechny výše uvedené adresy URL a níže uvedená nastavení.
Pro publikování změny vytvoříte novou verzi a učiníte ji aktuální. Platforma vždy čte aktuální verzi — adresy URL životního cyklu, adresy URL stavu a konfigurace a nastavení v tabulce — takže nová verze je může změnit (například začít vyžadovat variantu), aniž by se dotkla dřívějších verzí.
| Pole (na verzi) | Význam |
|---|---|
tag |
Odznak zobrazený na kartě doplňku v marketplace: new, recommended nebo priceOff. |
singleInstance |
Pokud je true, doplněk lze nainstalovat pouze jednou na organizaci. Viz Varianty a jedna instalace. |
requireFlavours |
Seznam variant, které organizace musí již mít, aby doplněk viděla a mohla jej nainstalovat. |
providesFlavours |
Seznam variant přidaných k organizaci při aktivaci doplňku. |
`tag`, `singleInstance`, `requireFlavours` a `providesFlavours` dříve patřily k samotnému doplňku. Nyní patří k verzi; existující hodnoty byly zkopírovány do každé verze doplňku. Při jejich čtení nebo úpravách vždy používejte verzi.
Varianty a jedna instalace
Varianty jsou štítky funkcí organizace (například replica nebo flexibee), které zapínají části produktu. Doplněk na nich může záviset a rozšiřovat je. Obě pole jsou JSON pole klíčů variant na verzi doplňku:
{
"singleInstance": true,
"requireFlavours": ["replica"],
"providesFlavours": ["flexibee"]
}
| Nastavení | Co se stane |
|---|---|
requireFlavours |
Doplněk je skryt ze seznamu marketplace pro organizaci, které chybí některá z těchto variant. Pokud uživatel otevře jeho detail přímo, upozornění vysvětlí, co chybí, a Aktivovat je zakázáno. Požadavek je zkontrolován znovu při aktivaci: pokud není splněn, instalace skončí ve stavu failed a uživatel obdrží chybu. |
providesFlavours |
Po úspěšné aktivaci (s activationUrl nebo bez ní) jsou chybějící varianty přidány k organizaci. |
singleInstance |
Jedna instalace na organizaci. Instalace ve stavu not-installed, installed nebo paused blokují novou; failed a uninstalled ne. Stránka detailu zakáže Aktivovat a aktivace je odmítnuta i na serveru. |
Když je doplněk odinstalován, odstraní se pouze varianty, které tato instalace přidala — nikdy varianty, které organizace měla dříve. Varianta také zůstane, pokud ji poskytuje jiný nainstalovaný nebo pozastavený doplněk stejné organizace.
Použijte `requireFlavours` pro doplněk, který dává smysl pouze vedle jiné funkce, a `providesFlavours` pro doplněk, který funkci zapíná. Doplněk, který potřebuje jednu instalaci na externí systém (například jednu na databázi), by měl nechat `singleInstance` vypnuté.
API token
Když platforma aktivuje váš doplněk, automaticky vygeneruje JWT bearer token vázaný na konkrétní instalaci. Tento token je předán na vaši activationUrl jako apiToken v aktivačním payloadu a měl by být bezpečně uložen vaší službou.
Použijte jej v následných voláních zpět do {productName} (např. čtení nebo zápis dat o produktech prostřednictvím Query API) jako standardní hlavičku Authorization: Bearer <token>.
Token je vymezen pro organizaci, která doplněk nainstalovala. Má oprávnění pro čtení/zápis a je platný neomezeně. Když je doplněk odinstalován, token automaticky vyprší do 15 minut.
tab: TypeScript
async function fetchProducts(apiToken: string, hostname: string): Promise<any[]> {
const response = await fetch(`{HOSTNAME}/api/query`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ query: 'products { id, name, price }' }),
});
if (!response.ok) throw new Error(`Query failed: ${response.status}`);
const data = await response.json();
return data.products ?? [];
}
---
tab: PHP
function fetchProducts(string $apiToken): array {
$ch = curl_init("{hostname}/api/query");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $apiToken",
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['query' => 'products { id, name, price }']),
]);
$body = curl_exec($ch);
curl_close($ch);
$data = json_decode($body, true);
return $data['products'] ?? [];
}
URL adresy životního cyklu
activationUrl — Doplněk nainstalován
Voláno jednou, když uživatel nainstaluje váš doplněk. Použijte jej k zřízení zdrojů (např. vytvoření tenanta/pracovního prostoru ve vaší službě), uložení apiToken a přípravě jakékékoli počáteční konfigurace.
Metoda: POST
Očekávaná odpověď: { "success": true } — jakákoli neúspěšná odpověď nebo chyba HTTP způsobí selhání instalace.
Do odpovědi můžete zahrnout volitelné pole `data`. Jeho hodnota (jakýkoli objekt serializovatelný do JSON) bude předána beze změny do frontendové odpovědi, což je užitečné pro ladění problémů s aktivací z vývojářských nástrojů prohlížeče.
```json
{ "success": true, "data": { "workspaceId": "ws-123", "plan": "trial" } }
**Request payload:**
```typescript
interface ActivationPayload {
myAddon_id: string; // Unique ID of this installation
addon_id: string; // Addon definition ID
tenant_id: string; // {productName} tenant ID
organisation_id: string; // Organisation that installed the addon
// API token — store this to authenticate future calls back to {productName}
apiToken: string; // JWT bearer token
access_token: string; // Same as apiToken (alias for compatibility)
api_token_id: string; // Database ID of the token record
// Signing secret — store this to verify ps_token JWTs on incoming configUrl / urls[] / statusUrl requests
psSigningSecret: string;
organisation: {
id: string;
code: string;
name: string;
email: string | null;
vatId: string | null;
registrationNumber: string | null;
taxId: string | null;
defaultLanguage: string; // e.g. "en", "cs"
defaultTimezone: string; // e.g. CET
flavours: Record<string, any> | null;
country: string | null; // ISO country code
};
tenant: {
id: string;
code: string;
name: string;
};
user: {
id: string;
username: string;
firstname: string | null;
surname: string | null;
email: string | null;
telephone: string | null;
jobTitle: string | null;
};
}
tab: TypeScript
import express, { Request, Response } from 'express';
const app = express();
app.use(express.json());
// In-memory store (use a real database in production)
const installations = new Map<string, {
apiToken: string;
psSigningSecret: string;
organisationId: string;
}>();
app.post('/addon/activate', (req: Request, res: Response) => {
const { myAddon_id, apiToken, psSigningSecret, organisation } = req.body;
if (!myAddon_id || !apiToken || !psSigningSecret) {
return res.status(400).json({ success: false, error: 'Missing required fields' });
}
// Store both the API token and the signing secret
installations.set(myAddon_id, {
apiToken,
psSigningSecret,
organisationId: organisation.id,
});
console.log(`Addon installed for org: ${organisation.name} (${organisation.id})`);
res.json({ success: true });
});
---
tab: PHP
// activate.php
$payload = json_decode(file_get_contents('php://input'), true);
$myAddonId = $payload['myAddon_id'] ?? null;
$apiToken = $payload['apiToken'] ?? null;
$psSigningSecret = $payload['psSigningSecret'] ?? null;
$organisation = $payload['organisation'] ?? null;
if (!$myAddonId || !$apiToken || !$psSigningSecret) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Missing required fields']);
exit;
}
// Persist installation (use a real DB in production)
file_put_contents(
__DIR__ . "/installations/$myAddonId.json",
json_encode([
'apiToken' => $apiToken,
'psSigningSecret' => $psSigningSecret,
'organisationId' => $organisation['id'],
])
);
echo json_encode(['success' => true]);
deactivationUrl — Doplněk odinstalován
Voláno, když uživatel odinstaluje doplněk. Vyčistěte zřízené zdroje a zneplatněte všechny uložené tokeny.
Metoda: POST
Očekávaná odpověď: { "success": true } — můžete zahrnout volitelné pole data (jakýkoli objekt JSON), které bude předáno do frontendové odpovědi a je užitečné pro ladění (viditelné ve vývojářských nástrojích prohlížeče).
Odpověď 404 z vašeho koncového bodu je považována za úspěch (idempotentní odinstalace). Pokud vrátíte neúspěšnou odpověď, platforma zaznamená chybu, ale doplněk přesto označí jako odinstalovaný.
Payload požadavku:
interface DeactivationPayload {
myAddon_id: string;
organisation_id: string;
}
tab: TypeScript
app.post('/addon/deactivate', (req: Request, res: Response) => {
const { myAddon_id } = req.body;
installations.delete(myAddon_id);
console.log(`Addon uninstalled: ${myAddon_id}`);
res.json({ success: true });
});
---
tab: PHP
// deactivate.php
$payload = json_decode(file_get_contents('php://input'), true);
$myAddonId = $payload['myAddon_id'] ?? null;
if ($myAddonId) {
@unlink(__DIR__ . "/installations/$myAddonId.json");
}
echo json_encode(['success' => true]);
pauseUnpauseUrl — Doplněk pozastaven / obnoven
Voláno, když uživatel pozastaví nebo obnoví doplněk. Pozastavení/obnovení používá stejnou URL adresu — akci rozlišíte pomocí pole action v payloadu.
Metoda: POST
Očekávaná odpověď: { "success": true } — můžete zahrnout volitelné pole data (jakýkoli objekt JSON), které bude předáno do frontendové odpovědi a je užitečné pro ladění (viditelné ve vývojářských nástrojích prohlížeče).
Payload požadavku:
interface PauseUnpausePayload {
myAddon_id: string;
addon_id: string;
tenant_id: string;
organisation_id: string;
action: 'pause' | 'unpause';
organisation: { /* same shape as in activation */ };
tenant: { /* same shape as in activation */ };
// Note: user is NOT included in pause/unpause payloads
}
tab: TypeScript
app.post('/addon/pause-unpause', (req: Request, res: Response) => {
const { myAddon_id, action } = req.body;
if (action !== 'pause' && action !== 'unpause') {
return res.status(400).json({ success: false, error: 'Invalid action' });
}
const installation = installations.get(myAddon_id);
if (!installation) {
return res.status(404).json({ success: false, error: 'Installation not found' });
}
// Pause or resume your background jobs / sync processes here
if (action === 'pause') {
console.log(`Pausing addon ${myAddon_id}`);
} else {
console.log(`Resuming addon ${myAddon_id}`);
}
res.json({ success: true });
});
---
tab: PHP
// pause-unpause.php
$payload = json_decode(file_get_contents('php://input'), true);
$myAddonId = $payload['myAddon_id'] ?? null;
$action = $payload['action'] ?? null;
if (!in_array($action, ['pause', 'unpause'], true)) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Invalid action']);
exit;
}
// Store desired state, adjust background jobs, etc.
$state = $action === 'pause' ? 'paused' : 'active';
// ... your business logic here ...
echo json_encode(['success' => true]);
configUrl — Konfigurační UI
Konfigurační adresa URL se otevře, když uživatel klikne na tlačítko Konfigurovat na stránce s detaily doplňku. Nejedná se o webhook – vaše adresa URL se otevře přímo v prohlížeči. Způsob zobrazení můžete zvolit pomocí configUrlType.
configUrlType: "link" vs configUrlType: "iframe"
| Typ | Chování |
|---|---|
link |
Adresa URL se otevře v nové záložce prohlížeče. Uživatel opustí uživatelské rozhraní {productName}. Vhodné pro složité vícefázové průvodce konfigurací. |
iframe |
Adresa URL je vložena do dialogu uvnitř uživatelského rozhraní {productName}. Uživatel zůstává v kontextu. Vhodné pro jednoduché formuláře nastavení. Vaše stránka může dialog sama zavřít, viz Zavření konfiguračního dialogu. |
Při použití `iframe` se ujistěte, že vaše stránka nastavuje příslušné hlavičky `Content-Security-Policy` a `X-Frame-Options` (nebo je vynechává), aby bylo umožněno vložení. Omezující `X-Frame-Options: DENY` zabrání načtení iframe.
Před otevřením vaší configUrl v prohlížeči platforma připojí následující parametry dotazu:
| Parametr | Popis |
|---|---|
ps_token |
Krátkodobý JWT (10 min) podepsaný psSigningSecret z aktivace. Použijte jej k ověření, že požadavek pochází z platformy. |
myAddon_id |
ID instalace doplňku. |
organisation_id |
ID organizace. |
tenant_id |
ID tenanta. |
user_id |
ID uživatele, který spustil akci. |
language |
Aktuální jazyk uživatelského rozhraní uživatele (např. en, cs). |
JWT `ps_token` obsahuje dvě tvrzení: `sub` (`myAddon_id`) a `org` (`organisation_id`). Jednoduché parametry dotazu jsou poskytovány pro pohodlí – vždy ověřte podpis JWT, než jim budete důvěřovat.
Příklad polí verze doplňku:
{
"configUrl": "https://your-addon.example.com/config",
"configUrlType": "iframe"
}
tab: TypeScript
import jwt from 'jsonwebtoken';
app.get('/config', (req: Request, res: Response) => {
const psConfig = req.query.ps_token as string | undefined;
const myAddonId = req.query.myAddon_id as string | undefined;
if (!psConfig || !myAddonId) {
return res.status(401).send('<p>Missing ps_token</p>');
}
const installation = installations.get(myAddonId);
if (!installation) {
return res.status(403).send('<p>Unknown installation</p>');
}
// Verify signature and expiry using the stored psSigningSecret
try {
jwt.verify(psConfig, installation.psSigningSecret);
} catch {
return res.status(401).send('<p>Invalid or expired ps_token</p>');
}
res.send(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Addon Configuration</title>
<style>body { font-family: sans-serif; padding: 1rem; }</style>
</head>
<body>
<h2>Configure Addon</h2>
<form method="POST" action="/config/save">
<input type="hidden" name="myAddon_id" value="${myAddonId}">
<label>Sync interval (minutes):
<input type="number" name="interval" value="30" min="5">
</label>
<button type="submit">Save</button>
</form>
</body>
</html>
`);
});
---
tab: PHP
// config.php
// Requires firebase/php-jwt: composer require firebase/php-jwt
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
$psConfig = $_GET['ps_token'] ?? null;
$myAddonId = $_GET['myAddon_id'] ?? null;
if (!$psConfig || !$myAddonId) {
http_response_code(401);
echo '<p>Missing ps_token</p>';
exit;
}
$installationFile = __DIR__ . "/installations/$myAddonId.json";
if (!file_exists($installationFile)) {
http_response_code(403);
echo '<p>Unknown installation</p>';
exit;
}
$installation = json_decode(file_get_contents($installationFile), true);
$psSigningSecret = $installation['psSigningSecret'] ?? null;
// Verify signature and expiry
try {
JWT::decode($psConfig, new Key($psSigningSecret, 'HS256'));
} catch (\Exception $e) {
http_response_code(401);
echo '<p>Invalid or expired ps_token: ' . htmlspecialchars($e->getMessage()) . '</p>';
exit;
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Addon Configuration</title>
<style>body { font-family: sans-serif; padding: 1rem; }</style>
</head>
<body>
<h2>Configure Addon</h2>
<form method="POST" action="/config/save.php">
<input type="hidden" name="myAddon_id" value="<?= htmlspecialchars($myAddonId) ?>">
<label>Sync interval (minutes):
<input type="number" name="interval" value="30" min="5">
</label>
<button type="submit">Save</button>
</form>
</body>
</html>
Zavření konfiguračního dialogu
S configUrlType: "iframe" může vaše stránka dialog sama zavřít, typicky hned poté, co uložila konfiguraci. Odešlete zprávu nadřazenému oknu:
// after your "save" request succeeded
window.parent.postMessage({ type: 'productsync:addon:close-config', saved: true }, '*');
saved: trueje volitelné. Pokud je nastaveno, {productName} po zavření zobrazí „Konfigurace uložena“ — zpráva zobrazená uvnitř vaší stránky by zmizela spolu s dialogem.- Odešlete ji pouze po úspěšném uložení. Pokud se uložení nezdaří, nic neposílejte: dialog zůstane otevřený, aby si uživatel mohl přečíst chybu a opravit formulář.
- Zpráva je respektována pouze tehdy, když pochází ze samotného konfiguračního iframe, bez ohledu na jeho původ, takže nic jiného nemůže dialog tímto způsobem zavřít.
- Ať už je dialog zavřen jakkoli — vaší zprávou, křížkem nebo klávesou Esc — stránka detailu doplňku je znovu načtena: stav, průběh a akce jsou znovu načteny z vaší
statusUrl. Uživatel okamžitě vidí účinek nové konfigurace, takže se ujistěte, že vašestatusUrljiž odráží to, co jste právě uložili.
statusUrl — Stav doplňku
Pokud poskytnete statusUrl, platforma ji zavolá, aby uživateli zobrazila aktuální stav vašeho doplňku (zobrazeno v postranním panelu detailu doplňku s barevnou ikonou). Stejná odpověď může také nést průběh dlouhotrvající úlohy a akční tlačítka (například „Synchronizovat nyní“), viz níže.
Metoda: GET
Autentizace: Před voláním vaší statusUrl platforma připojí následující parametry dotazu:
| Parametr | Popis |
|---|---|
ps_token |
Krátkodobý JWT (10 min) podepsaný psSigningSecret z aktivace. Použijte jej k ověření, že požadavek pochází z platformy. |
myAddon_id |
ID instalace doplňku. |
organisation_id |
ID organizace. |
tenant_id |
ID tenanta. |
user_id |
ID uživatele, který spustil akci. |
language |
Aktuální jazyk uživatelského rozhraní uživatele (např. en, cs). |
JWT `ps_token` obsahuje dvě tvrzení: `sub` (`myAddon_id`) a `org` (`organisation_id`). Jednoduché parametry dotazu jsou poskytovány pro pohodlí – vždy ověřte podpis JWT, než jim budete důvěřovat.
Formát odpovědi:
interface AddonStatusResponse {
/** Current state of the addon */
status: 'ok' | 'paused' | 'blocked' | 'invalid-auth' | 'error' | 'unknown';
/** ISO 8601 timestamp of the last successful operation, or null */
lastSuccessAt: string | null;
/** ISO 8601 timestamp of the last failure, or null */
lastFailureAt: string | null;
/**
* Optional human-readable message describing the current state.
* Can be a plain string or a TranslatableString object (language → message map).
*/
statusMessage: string | Record<string, string> | null;
/**
* Optional arbitrary data passed through to the frontend response as-is.
* Not shown in the UI — visible only in the browser's developer tools (Network tab).
* Useful for surfacing diagnostic information without a separate debug endpoint.
*/
data?: Record<string, any>;
/**
* Optional. Progress of a job that is running right now; leave it out (or null) when nothing runs.
* Shown as a progress bar under the status.
*/
progress?: {
/** Short text of what is happening, in the language of the `language` parameter (e.g. "Reading data"). */
phase: string | null;
/** 0-100. Values outside the range are clamped. */
percent: number;
/** Estimated seconds left, or null when unknown. */
etaSeconds: number | null;
} | null;
/** Optional. Buttons shown under the status, see "Actions". */
actions?: AddonAction[];
}
Hodnoty stavu a jejich význam:
| Stav | Barva | Význam |
|---|---|---|
ok |
Zelená | Doplněk běží normálně |
paused |
Žlutá | Doplněk je pozastaven uživatelem |
blocked |
Oranžová | Zpracování je blokováno (např. omezení rychlosti, problém s upstreamem) |
invalid-auth |
Červená | Autentizace k externí službě selhala |
error |
Červená | Došlo k neočekávané chybě |
unknown |
Šedá | Stav nelze určit, typicky proto, že doplněk ještě není nakonfigurován. Přidejte statusMessage, který říká, co dělat (např. „Nejprve nakonfigurujte připojení“). |
tab: TypeScript
app.get('/addon/status', (req: Request, res: Response) => {
const psConfig = req.query.ps_token as string | undefined;
const myAddonId = req.query.myAddon_id as string | undefined;
if (!psConfig || !myAddonId) {
return res.status(401).json({ error: 'Missing ps_token' });
}
const installation = installations.get(myAddonId);
if (!installation) {
return res.status(403).json({ error: 'Unknown installation' });
}
// Verify signature and expiry using the stored psSigningSecret
try {
jwt.verify(psConfig, installation.psSigningSecret);
} catch {
return res.status(401).json({ error: 'Invalid or expired ps_token' });
}
// Determine your actual status (query your DB, check job queue, etc.)
const isHealthy = checkHealth(myAddonId);
res.json({
status: isHealthy ? 'ok' : 'error',
lastSuccessAt: isHealthy ? new Date().toISOString() : null,
lastFailureAt: isHealthy ? null : new Date().toISOString(),
statusMessage: isHealthy
? { en: 'Running normally', cs: 'Běží normálně' }
: { en: 'Sync failed — check credentials', cs: 'Synchronizace selhala — zkontrolujte přihlašovací údaje' },
});
});
function checkHealth(_myAddonId: string): boolean {
// Your actual health check logic here
return true;
}
---
tab: PHP
// status.php
// Requires firebase/php-jwt: composer require firebase/php-jwt
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
$psConfig = $_GET['ps_token'] ?? null;
$myAddonId = $_GET['myAddon_id'] ?? null;
if (!$psConfig || !$myAddonId) {
http_response_code(401);
echo json_encode(['error' => 'Missing ps_token']);
exit;
}
$installationFile = __DIR__ . "/installations/$myAddonId.json";
if (!file_exists($installationFile)) {
http_response_code(403);
echo json_encode(['error' => 'Unknown installation']);
exit;
}
$installation = json_decode(file_get_contents($installationFile), true);
$psSigningSecret = $installation['psSigningSecret'] ?? null;
// Verify signature and expiry
try {
JWT::decode($psConfig, new Key($psSigningSecret, 'HS256'));
} catch (\Exception $e) {
http_response_code(401);
echo json_encode(['error' => 'Invalid or expired ps_token: ' . $e->getMessage()]);
exit;
}
// Return status
header('Content-Type: application/json');
echo json_encode([
'status' => 'ok',
'lastSuccessAt' => date('c'),
'lastFailureAt' => null,
'statusMessage' => [
'en' => 'Running normally',
'cs' => 'Běží normálně',
],
]);
Průběh dlouhotrvající úlohy
Když váš doplněk spustí něco, co chvíli trvá (synchronizace, export, migrace), hlaste to v progress po dobu, co běží:
{
"status": "ok",
"lastSuccessAt": "2026-10-08T12:00:00Z",
"lastFailureAt": null,
"statusMessage": null,
"progress": { "phase": "Reading data", "percent": 42, "etaSeconds": 180 }
}
- Platforma vykreslí pruh s procenty,
phasea zbývajícím časem pod stavem. - Dokud je detail doplňku otevřen, platforma se dotazuje na vaši
statusUrlznovu každé 4 sekundy, dokud odpověď obsahujeprogress. Udržujte volání stavu nenáročné a pošleteprogress: null(nebo ho vynechejte), jakmile úloha skončí – pruh zmizí a dotazování se zpomalí. - Zajistěte, aby
percentpouze rostlo, dokud úloha běží; pruh, který se vrací zpět, vypadá jako selhání. - Když úloha skončí, nahlaste její výsledek ve
status,lastSuccessAt,lastFailureAtastatusMessage.
Akce
Doplněk může nabízet tlačítka na své stránce detailu, například Synchronizovat nyní nebo Znovu spustit plnou synchronizaci. Jsou součástí odpovědi o stavu, takže váš doplněk nepotřebuje vlastní obrazovku. Přidejte je do actions:
interface AddonAction {
/** Unique within your addon. */
id: string;
/** Text of the button: a string or a language -> text map (see TranslatableString). */
label: string | Record<string, string>;
/** Optional icon name, e.g. 'play' or 'refresh-cw'. Icons the platform does not ship are not drawn. */
icon?: string;
/** Optional: 'default' (primary), 'outline' or 'destructive'. */
variant?: 'default' | 'outline' | 'destructive';
/** Shown but not clickable, e.g. while a run is already in progress. Also refused by the server. */
disabled?: boolean;
/** When set, the user has to confirm first. All texts may be strings or language maps. */
confirm?: { title: string | Record<string, string>; text?: string | Record<string, string>; confirmLabel?: string | Record<string, string> };
/** HTTPS URL the platform POSTs to when the button is clicked. Never sent to the browser. */
url: string;
}
{
"status": "ok",
"lastSuccessAt": "2026-10-08T12:00:00Z",
"lastFailureAt": null,
"statusMessage": null,
"actions": [
{ "id": "sync", "label": { "en": "Synchronize now", "cs": "Synchronizovat teď" }, "icon": "play", "url": "https://addon.example.com/addon/actions/sync" },
{
"id": "full-resync", "label": { "en": "Re-run full synchronization", "cs": "Spustit úplnou synchronizaci znovu" }, "icon": "refresh-cw", "variant": "outline",
"url": "https://addon.example.com/addon/actions/full-resync",
"confirm": { "title": { "en": "Re-run everything?", "cs": "Spustit vše znovu?" }, "text": { "en": "This can take a long time.", "cs": "Může to trvat dlouho." } }
}
]
}
Co se stane, když uživatel klikne na tlačítko:
- Platforma se znovu dotáže na vaši
statusUrla vyhledá akci podleid. Spustit lze pouze akci, kterou váš stav inzeruje právě teď a která nenídisabled– prohlížeč nikdy neuvádí URL ani příkaz. Chybějící akce odpoví „Neznámá akce“, zakázaná akce odpoví „Tato akce není nyní dostupná“. - Pokud má akce
confirm, uživatel ji nejprve potvrdí. - Platforma odešle
POSTs JSON tělem{"myAddon_id": "...", "action": "<id>"}naurlakce. URL je podepsána přesně jako volání stavu: jsou připojeny stejné parametry dotazu (ps_token,myAddon_id,organisation_id,tenant_id,user_id,language) aps_tokenověříte stejným způsobem.urlodkazuje na vaši službu a může obsahovat proměnnou šablony${myAddon.id}. - Jakákoli odpověď
2xxje úspěch. Cokoli jiného se uživateli zobrazí jako chyba („Doplněk odpověděl HTTP 500“). Platforma čeká na odpověď maximálně 30 sekund, proto spusťte dlouhou práci na pozadí, okamžitě odpovězte a nahlaste běžící úlohu prostřednictvímprogress. - Stránka znovu načte váš stav, takže tlačítko, které spustilo úlohu, se stane
disableda objeví se ukazatel průběhu.
Platforma nekontroluje roli uživatele pro externí akci: každý uživatel, který může otevřít detail doplňku, na něj může kliknout. Volání nese `user_id`; rozhodněte ve svém endpointu, kdo smí co dělat, a nevystavujte destruktivní akce bez `confirm`.
tab: TypeScript
// the status answer advertises the buttons; a running job disables them
app.get('/addon/status', (req: Request, res: Response) => {
// ... verify ps_token as above ...
const running = isJobRunning(req.query.myAddon_id as string);
res.json({
status: 'ok',
lastSuccessAt: lastSuccess(),
lastFailureAt: null,
statusMessage: null,
progress: running ? { phase: 'Reading data', percent: jobPercent(), etaSeconds: null } : null,
actions: [
{ id: 'sync', label: { en: 'Synchronize now', cs: 'Synchronizovat teď' }, icon: 'play', disabled: running, url: `${PUBLIC_URL}/addon/actions/sync` },
],
});
});
// the platform POSTs here when the user clicks the button
app.post('/addon/actions/:id', express.json(), (req: Request, res: Response) => {
// ... verify ps_token as above (same query parameters as the status call) ...
if (req.params.id !== req.body.action) return res.status(400).json({ error: 'Action mismatch' });
startJobInBackground(req.body.myAddon_id); // do not wait for the job
res.status(202).json({ started: true }); // any 2xx is success
});
---
tab: PHP
// status.php: the answer advertises the buttons
$running = isJobRunning($myAddonId);
echo json_encode([
'status' => 'ok',
'lastSuccessAt' => lastSuccess(),
'lastFailureAt' => null,
'statusMessage' => null,
'progress' => $running ? ['phase' => 'Reading data', 'percent' => jobPercent(), 'etaSeconds' => null] : null,
'actions' => [[
'id' => 'sync',
'label' => ['en' => 'Synchronize now', 'cs' => 'Synchronizovat teď'],
'icon' => 'play',
'disabled' => $running,
'url' => PUBLIC_URL . '/addon/actions/sync.php',
]],
]);
// actions/sync.php: the platform POSTs here when the user clicks the button
// ... verify ps_token as above (same query parameters as the status call) ...
$body = json_decode(file_get_contents('php://input'), true);
startJobInBackground($body['myAddon_id']); // do not wait for the job
http_response_code(202); // any 2xx is success
echo json_encode(['started' => true]);
Seznam URL
Kromě configUrl můžete definovat pole dalších odkazů (urls), které se zobrazí v postranním panelu stránky s detaily doplňku. Tyto odkazy jsou užitečné pro hluboké odkazy do vaší služby, jako jsou prohlížeče protokolů, sestavy nebo dashboardy.
{
"urls": [
{
"sortOrder": 1,
"name": {
"en": "Sync Logs",
"cs": "Logy synchronizace"
},
"icon": "list",
"url": "https://your-addon.example.com/logs?addon=${myAddon.id}",
"urlType": "iframe"
},
{
"sortOrder": 2,
"name": {
"en": "Open Dashboard",
"cs": "Otevřít přehled"
},
"icon": "external-link",
"url": "https://your-addon.example.com/dashboard",
"urlType": "link"
}
]
}
Pole:
| Pole | Typ | Popis |
|---|---|---|
sortOrder |
number |
Pořadí zobrazení v postranním panelu (vzestupně) |
name |
TranslatableString |
Popisek tlačítka – objekt s kódy jazyků jako klíči |
icon |
string |
Název ikony (identifikátor ikony Lucide, např. list, history, settings) |
url |
string |
Cílová adresa URL. Podporuje proměnné šablony (viz níže). |
urlType |
"link" | "iframe" |
Jak se adresa URL otevírá (viz níže) |
Proměnné šablony URL
Pole url podporuje následující proměnné šablony, které jsou nahrazeny na straně serveru před odesláním URL do prohlížeče:
| Proměnná | Nahrazeno čím |
|---|---|
${HOSTNAME} |
Veřejná základní adresa URL platformy (např. https://app.example.com) |
${myAddon.id} |
myAddon_id aktuální instalace |
Platforma také připojí následující parametry dotazu ke každé adrese URL před jejím otevřením:
| Parametr | Popis |
|---|---|
ps_token |
Krátkodobý JWT (10 min) podepsaný pomocí psSigningSecret z aktivace. Použijte jej k ověření, že požadavek pochází z platformy. |
myAddon_id |
ID instalace doplňku. |
organisation_id |
ID organizace. |
tenant_id |
ID tenanta. |
user_id |
ID uživatele, který spustil akci. |
language |
Aktuální jazyk uživatelského rozhraní uživatele (např. en, cs). |
JWT `ps_token` obsahuje dva nároky: `sub` (`myAddon_id`) a `org` (`organisation_id`). Prosté parametry dotazu jsou poskytovány pro pohodlí – vždy ověřte podpis JWT, než kterémukoli z nich důvěřujete.
urlType: "link" vs urlType: "iframe"
| Typ | Chování |
|---|---|
link |
Kliknutím na tlačítko se URL otevře v nové záložce prohlížeče. Uživatel opustí {productName}. |
iframe |
Kliknutím na tlačítko se URL otevře inline jako vložený panel uvnitř stránky s detaily doplňku. Uživatel zůstává v kontextu. |
Použijte iframe pro zobrazení, která jsou určena ke konzumaci bez opuštění platformy (např. protokol vyvolání nebo řídicí panel stavu). Použijte link, když je cílem plnohodnotná externí aplikace, kterou nemá smysl vkládat (např. váš administrátorský panel).
Přeložitelný řetězec
Několik polí (name, shortDescription atd.) podporuje více jazyků. Poskytněte objekt JSON s jazykovými značkami IETF jako klíči:
{
"en": "Sync Logs",
"cs": "Logy synchronizace",
"de": "Synchronisierungsprotokolle",
"sk": "Logy synchronizácie",
"pl": "Dzienniki synchronizacji"
}
Platforma vybere řetězec odpovídající jazyku uživatele a v případě, že požadovaný jazyk není k dispozici, vrátí se k en.
Podporované jazyky: en, cs, de, sk, pl, hu, fr, it, es, ro, hr, ua, pt, sl
Vestavěné doplňky
Doplňky, které {productName} poskytuje sám (například synchronizace ABRA Flexi a Pohoda), dodržují stejnou smlouvu jako doplňky, které vytváříte, takže uživatel vidí jednu konzistentní stránku detailu doplňku. Rozdíl je pouze v tom, kde se práce děje: místo adres HTTPS, jejich pole životního cyklu (activationUrl, deactivationUrl, pauseUnpauseUrl, statusUrl, configUrl) obsahují hodnotu builtin:, což znamená „udělej to uvnitř platformy“. Jejich konfigurační stránka je stránka platformy zobrazená ve stejném dialogu a zavírá se stejným způsobem, jak je popsáno v Zavření konfiguračního dialogu.
Ve svém vlastním doplňku nikdy nepoužíváte builtin:. Je to pro vás důležité jako reference: cokoli, co vestavěný doplněk zobrazuje – stav, ukazatel průběhu, tlačítka Spustit a Znovu spustit, Pozastavit – váš doplněk může zobrazit prostřednictvím statusUrl, progress, actions a pauseUnpauseUrl.
Formát odpovědi
Všechny koncové body životního cyklu (activationUrl, deactivationUrl, pauseUnpauseUrl) musí vracet JSON s minimálně booleovskou hodnotou success:
{ "success": true }
V případě selhání uveďte chybovou zprávu error:
{ "success": false, "error": "Failed to provision workspace: quota exceeded" }
Vraťte příslušný stavový kód HTTP:
200— úspěch400— chybný požadavek (neplatná nebo chybějící pole datové části)500— neočekávaná chyba serveru
Platforma považuje jakýkoli stav HTTP, který není 2xx, nebo odpověď { "success": false } za selhání. Pro activationUrl to způsobí selhání a vrácení instalace. Pro deactivationUrl se chyba zaznamená, ale odinstalace se přesto dokončí. Pro pauseUnpauseUrl se chyba zaznamená a aktualizace stavu se neprojeví.
Ladění pomocí data
Jakákoli odpověď JSON z koncového bodu životního cyklu (activationUrl, deactivationUrl, pauseUnpauseUrl, statusUrl) může obsahovat volitelné pole data. Platforma tuto hodnotu předá beze změny do odpovědi frontendu, čímž se stane viditelnou v nástrojích pro vývojáře prohlížeče (záložka Síť). To je záměrné a představuje pohodlný způsob, jak zobrazit interní stav nebo diagnostické informace, aniž by bylo nutné mít samostatný koncový bod pro ladění.
// Example: activation response with debug data
{
"success": false,
"error": "Quota exceeded",
"data": {
"currentUsage": 42,
"limit": 40,
"resetAt": "2026-03-01T00:00:00Z"
}
}
Pole data se nikdy nezobrazuje v uživatelském rozhraní {productName} – je přítomno pouze v nezpracovaných odpovědích API. Neukládejte sem citlivá uživatelská data; zacházejte s ním jako s diagnostickým kanálem pouze pro vývojáře.