API pro vývojáře doplňků

Tento průvodce vysvětluje, jak vytvořit doplněk pro tržiště {productName}. Váš doplněk je v systému registrován se sadou URL adres, které platforma volá ve specifický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 adres. Každé pole je volitelné; uveďte pouze ty, které váš doplněk vyžaduje.

PoleKdy se voláMetoda
activationUrlUživatel nainstaluje doplněkPOST
deactivationUrlUživatel odinstaluje doplněkPOST
pauseUnpauseUrlUživatel pozastaví nebo obnoví doplněkPOST
configUrlUživatel klikne na “Konfigurovat”Otevře se v prohlížeči (iframe nebo odkaz)
statusUrlPlatforma se dotazuje na aktuální stav doplňkuGET

Pole urls navíc obsahuje další odkazy, které se zobrazují v postranním panelu s podrobnostmi o doplňku (např. protokoly, sestavy). Viz sekce Seznam URL adres níže.


API Token

Když platforma aktivuje váš doplněk, vygeneruje nosný token JWT spojený s touto konkrétní instalací. Tento token je předán vaší adrese activationUrl jako apiToken v aktivační datové části 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 produktu prostřednictvím Query API) jako standardní hlavičku Authorization: Bearer <token>.

Token je vymezen na organizaci, která doplněk nainstalovala. Má oprávnění pro čtení/zápis a je platný neomezeně dlouho. 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 byl nainstalován

Volá se jednou, když uživatel nainstaluje váš doplněk. Použijte jej k zajištění prostředků (např. vytvoření tenanta/pracovního prostoru ve vaší službě), uložení apiToken a přípravě jakékoli počáteční konfigurace.

Metoda: POST
Očekávaná odpověď: { "success": true } – jakákoli jiná 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 front-endové 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" } }

**Datová část požadavku:**

```typescript
interface ActivationPayload {
    myAddon_id:    string;   // Jedinečné ID této instalace
    addon_id:      string;   // ID definice doplňku
    tenant_id:     string;   // ID tenanta {productName}
    organisation_id: string; // Organizace, která nainstalovala doplněk

    // API token – uložte si jej pro ověření budoucích volání zpět do {productName}
    apiToken:        string;   // Nosný token JWT
    access_token:    string;   // Stejné jako apiToken (alias pro kompatibilitu)
    api_token_id:    string;   // ID záznamu tokenu v databázi

    // Podpisový klíč – uložte si jej pro ověření JWT ps_token u příchozích požadavků configUrl / urls[] / statusUrl
    psSigningSecret: string;

    organisation: {
        id:                  string;
        code:                string;
        name:                string;
        email:               string | null;
        vatId:               string | null;
        registrationNumber:  string | null;
        taxId:               string | null;
        defaultLanguage:     string;       // např. "en", "cs"
        defaultTimezone:     string;       // např. CET
        flavours:            Record<string, any> | null;
        country:             string | null; // ISO kód země
    };

    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 byl odinstalován

Volá se, když uživatel odinstaluje doplněk. Vyčistěte zajištěné prostředky 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 front-endové 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 jinou než úspěšnou odpověď, platforma zaprotokoluje chybu, ale stále označí doplněk jako odinstalovaný.

Datová část 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 byl pozastaven / obnoven

Volá se, když uživatel pozastaví nebo obnoví doplněk. Pozastavení/obnovení používá stejnou URL adresu – rozlišujte akci pomocí pole action v datové části.

Metoda: POST
Očekávaná odpověď: { "success": true } – můžete zahrnout volitelné pole data (jakýkoli objekt JSON), které bude předáno do front-endové odpovědi a je užitečné pro ladění (viditelné ve vývojářských nástrojích prohlížeče).

Datová část požadavku:

interface PauseUnpausePayload {
    myAddon_id:      string;
    addon_id:        string;
    tenant_id:       string;
    organisation_id: string;
    action:          'pause' | 'unpause';

    organisation: { /* stejný tvar jako v aktivaci */ };
    tenant:       { /* stejný tvar jako v aktivaci */ };
    // Poznámka: uživatel NENÍ zahrnut v datových částech pozastavení/obnovení
}
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í URL adresa se otevře, když uživatel klikne na tlačítko Konfigurovat na stránce s podrobnostmi o doplňku. Nejedná se o webhook – vaše URL adresa se otevře přímo v prohlížeči. Můžete si vybrat, jak ji prezentovat, pomocí configUrlType.

TypChování
linkURL adresa se otevře v nové kartě prohlížeče. Uživatel opustí UI {productName}. Vhodné pro složité více stránkové průvodce konfigurací.
iframeURL adresa je vložena do inline panelu uvnitř UI {productName}. Uživatel zůstává v kontextu. Vhodné pro jednoduché formuláře nastavení.
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 možné ji vložit. Restriktivní `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:

ParametrPopis
ps_tokenKrátkodobý JWT (10 min) podepsaný pomocí psSigningSecret z aktivace. Použijte jej k ověření, že požadavek pochází z platformy.
myAddon_idID instalace doplňku.
organisation_idID organizace.
tenant_idID tenanta.
user_idID uživatele, který spustil akci.
languageAktuální jazyk UI uživatele (např. en, cs).
JWT `ps_token` obsahuje dvě deklarace: `sub` (`myAddon_id`) a `org` (`organisation_id`). Prosté parametry dotazu jsou poskytovány pro usnadnění – vždy ověřte podpis JWT předtím, 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>

statusUrl – Stav doplňku

Pokud zadáte statusUrl, platforma ji zavolá, aby uživateli zobrazila aktuální stav vašeho doplňku (zobrazený v postranním panelu s podrobnostmi o doplňku s barevně odlišenou ikonou).

Metoda: GET
Ověření: Před voláním vaší statusUrl platforma připojí následující parametry dotazu:

ParametrPopis
ps_tokenKrátkodobý JWT (10 min) podepsaný pomocí psSigningSecret z aktivace. Použijte jej k ověření, že požadavek pochází z platformy.
myAddon_idID instalace doplňku.
organisation_idID organizace.
tenant_idID tenanta.
user_idID uživatele, který spustil akci.
languageAktuální jazyk UI uživatele (např. en, cs).
JWT `ps_token` obsahuje dvě deklarace: `sub` (`myAddon_id`) a `org` (`organisation_id`). Prosté parametry dotazu jsou poskytovány pro usnadnění – vždy ověřte podpis JWT předtím, než jim budete důvěřovat.

Formát odpovědi:

interface AddonStatusResponse {
    /** Aktuální stav doplňku */
    status: 'ok' | 'paused' | 'blocked' | 'invalid-auth' | 'error';

    /** Časové razítko ISO 8601 poslední úspěšné operace nebo null */
    lastSuccessAt: string | null;

    /** Časové razítko ISO 8601 posledního selhání nebo null */
    lastFailureAt: string | null;

    /**
     * Volitelná zpráva čitelná pro člověka popisující aktuální stav.
     * Může to být prostý řetězec nebo objekt TranslatableString (mapa jazyk → zpráva).
     */
    statusMessage: string | Record<string, string> | null;

    /**
     * Volitelná libovolná data předaná do front-endové odpovědi beze změny.
     * Nezobrazuje se v UI – viditelné pouze ve vývojářských nástrojích prohlížeče (karta Síť).
     * Užitečné pro zobrazení diagnostických informací bez samostatného ladicího koncového bodu.
     */
    data?: Record<string, any>;
}

Hodnoty stavu a jejich význam:

StavBarvaVýznam
okZelenáDoplněk běží normálně
pausedŽlutáDoplněk je pozastaven uživatelem
blockedOranžováZpracování je blokováno (např. omezení rychlosti, problém s upstreamem)
invalid-authČervenáOvěření externí služby selhalo
errorČervenáDošlo k neočekávané chybě
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ě',
    ],
]);

Seznam URL adres

Kromě configUrl můžete definovat pole dalších odkazů (urls), které se zobrazí v postranním panelu stránky s podrobnostmi o doplňku. Ty jsou užitečné pro hluboké odkazy do vaší služby, jako jsou prohlížeče protokolů, sestavy nebo řídicí panely.

{
    "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:

PoleTypPopis
sortOrdernumberPořadí zobrazení v postranním panelu (vzestupně)
nameTranslatableStringPopisek tlačítka – objekt s kódy jazyků jako klíči
iconstringNázev ikony (identifikátor ikony Lucide, např. list, history, settings)
urlstringCílová URL adresa. Podporuje proměnné šablony (viz níže).
urlType"link" | "iframe"Jak se URL adresa otevírá (viz níže)

Proměnné šablony URL adres

Pole url podporuje následující proměnné šablony, které jsou nahrazeny na straně serveru před odesláním URL adresy do prohlížeče:

ProměnnáNahrazeno
${HOSTNAME}Veřejná základní URL adresa 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é URL adrese před jejím otevřením:

ParametrPopis
ps_tokenKrátkodobý JWT (10 min) podepsaný pomocí psSigningSecret z aktivace. Použijte jej k ověření, že požadavek pochází z platformy.
myAddon_idID instalace doplňku.
organisation_idID organizace.
tenant_idID tenanta.
user_idID uživatele, který spustil akci.
languageAktuální jazyk UI uživatele (např. en, cs).
JWT `ps_token` obsahuje dvě deklarace: `sub` (`myAddon_id`) a `org` (`organisation_id`). Prosté parametry dotazu jsou poskytovány pro usnadnění – vždy ověřte podpis JWT předtím, než jim budete důvěřovat.
TypChování
linkKliknutím na tlačítko se URL adresa otevře v nové kartě prohlížeče. Uživatel opustí {productName}.
iframeKliknutím na tlačítko se URL adresa otevře inline jako vložený panel uvnitř stránky s podrobnostmi o doplňku. Uživatel zůstává v kontextu.

Použijte iframe pro zobrazení, která mají být spotřebována bez opuštění platformy (např. protokol volání nebo řídicí panel stavu). Použijte link, když je cílem plnohodnotná externí aplikace, kterou nemá smysl vkládat (např. váš administrační panel).


TranslatableString

Několik polí (name, shortDescription atd.) podporuje více jazyků. Zadejte 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, použije en.

Podporované jazyky: en, cs, de, sk, pl, hu, fr, it, es, ro, hr, ua, pt, sl


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 jiný než 2xx nebo odpověď { "success": false } za selhání. Pro activationUrl to způsobí selhání instalace a vrácení zpět. Pro deactivationUrl je chyba zaprotokolována, ale odinstalace se stále dokončí. Pro pauseUnpauseUrl je chyba zaprotokolována 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ává beze změny do front-endové odpovědi, takže je viditelná ve vývojářských nástrojích prohlížeče (karta Síť). To je záměrné a je to pohodlný způsob, jak zobrazit interní stav nebo diagnostické informace bez nutnosti samostatného ladicího koncového bodu.

// Příklad: aktivační odpověď s ladicími daty
{
    "success": false,
    "error": "Quota exceeded",
    "data": {
        "currentUsage": 42,
        "limit": 40,
        "resetAt": "2026-03-01T00:00:00Z"
    }
}

Pole data se nikdy nezobrazuje v UI {productName} – je přítomno pouze v nezpracovaných odpovědích API. Neumísťujte sem citlivá uživatelská data; zacházejte s ním jako s diagnostickým kanálem pouze pro vývojáře.