ÚvodPlatformaCeníkOtázkyNápovědaKontakt Kivea FlexiKivea PohodaKivea účetnictvíKivea fakturace Vyzkoušet zdarma

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.

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: true je 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še statusUrl již 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, phase a zbývajícím časem pod stavem.
  • Dokud je detail doplňku otevřen, platforma se dotazuje na vaši statusUrl znovu každé 4 sekundy, dokud odpověď obsahuje progress. Udržujte volání stavu nenáročné a pošlete progress: null (nebo ho vynechejte), jakmile úloha skončí – pruh zmizí a dotazování se zpomalí.
  • Zajistěte, aby percent pouze rostlo, dokud úloha běží; pruh, který se vrací zpět, vypadá jako selhání.
  • Když úloha skončí, nahlaste její výsledek ve status, lastSuccessAt, lastFailureAt a statusMessage.

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:

  1. Platforma se znovu dotáže na vaši statusUrl a vyhledá akci podle id. 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á“.
  2. Pokud má akce confirm, uživatel ji nejprve potvrdí.
  3. Platforma odešle POST s JSON tělem {"myAddon_id": "...", "action": "<id>"} na url akce. 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) a ps_token ověříte stejným způsobem. url odkazuje na vaši službu a může obsahovat proměnnou šablony ${myAddon.id}.
  4. Jakákoli odpověď 2xx je ú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ím progress.
  5. Stránka znovu načte váš stav, takže tlačítko, které spustilo úlohu, se stane disabled a 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.
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ěch
  • 400 — 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.