Developer documentation

Build reliable wrapper-code workflows.

Learn how to authenticate, run wrapper codes, upload files, download artifacts, track usage, and integrate Zywrap into production applications.

API-first workflow guide

Move from wrapper discovery to production API calls.

Wrapper-code runsPOST /v2/runs
FilesUpload and attach inputs
ArtifactsDownload generated outputs
Production controlsKeys, retries, usage ledger
Back to SDKs
PHP

PHP SDK Guide

A complete, runnable example for integrating Zywrap Enterprise catalog metadata (including dynamic schemas) into your PHP application.

Option 1: Cloud Proxy (Quick Start)

Don't want to host Enterprise catalog metadata yourself? Use our official Composer package to connect directly to the Zywrap Cloud Proxy in seconds.

bash
composer require zywrapai/zywrap-php
php
<?php

$payload = [
    'wrapperCodes' => ['marketing_copywriting_cold_out_ema_b2b'],
    'model' => 'openai-gpt-4o-mini',
    'input' => [
        'text' => "Product: Zywrap
Audience: SaaS developers
Goal: Write a concise outbound email."
    ],
    'output' => ['format' => 'markdown', 'artifact' => true],
];

$ch = curl_init('https://api.zywrap.com/v2/runs');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('ZYWRAP_API_KEY'),
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS => json_encode($payload),
    CURLOPT_TIMEOUT => 120,
]);

$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($response === false || $status >= 400) {
    throw new Exception($response ?: curl_error($ch));
}

$data = json_decode($response, true);
echo $data['run']['outputText'];

Complete V2 file + artifact workflow

Use this when your app needs to upload a file, attach that file to a wrapper-code run, save the output as an artifact, and download the generated result.

php
<?php
// Complete V2 workflow: upload a file, run a wrapper, download the artifact.
// Keep zw_live keys on your server, not in browser/mobile clients.

$api = 'https://api.zywrap.com';
$apiKey = getenv('ZYWRAP_API_KEY');

if (!$apiKey) {
    throw new Exception('Missing ZYWRAP_API_KEY');
}

function zywrapRequest($method, $url, $headers = [], $body = null) {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CUSTOMREQUEST => $method,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_POSTFIELDS => $body,
        CURLOPT_TIMEOUT => 180,
    ]);

    $response = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    if ($response === false || $status >= 400) {
        throw new Exception($response ?: curl_error($ch));
    }
    curl_close($ch);
    return $response;
}

function uploadFile($api, $apiKey, $path) {
    $response = zywrapRequest('POST', $api . '/v2/files', [
        'Authorization: Bearer ' . $apiKey,
    ], [
        'file' => curl_file_create($path),
        'displayName' => 'Customer feedback CSV',
    ]);

    $payload = json_decode($response, true);
    return $payload['file']['id'] ?? $payload['id'];
}

function runWorkflow($api, $apiKey, $fileId) {
    $payload = [
        'wrapperCodes' => ['customer_support_reply_summary'],
        'model' => 'openai-gpt-4o-mini',
        'input' => [
            'text' => 'Summarize the attached customer feedback and list the top action items.',
            'files' => [$fileId],
        ],
        'output' => [
            'format' => 'markdown',
            'artifact' => true,
        ],
    ];

    $response = zywrapRequest('POST', $api . '/v2/runs', [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ], json_encode($payload));

    return json_decode($response, true)['run'];
}

function downloadArtifact($api, $apiKey, $artifactId, $outputPath = './zywrap-output.md') {
    $content = zywrapRequest('GET', $api . '/v2/artifacts/' . $artifactId . '/download', [
        'Authorization: Bearer ' . $apiKey,
    ]);
    file_put_contents($outputPath, $content);
    return $outputPath;
}

$fileId = uploadFile($api, $apiKey, './customer-feedback.csv');
$run = runWorkflow($api, $apiKey, $fileId);

echo 'Run status: ' . $run['status'] . PHP_EOL;
echo 'Output: ' . ($run['outputText'] ?? $run['outputSummary'] ?? '') . PHP_EOL;

if (!empty($run['artifacts'][0]['id'])) {
    $savedTo = downloadArtifact($api, $apiKey, $run['artifacts'][0]['id']);
    echo 'Artifact saved to: ' . $savedTo . PHP_EOL;
}
?>
OR

Option 2: Advanced Local Database Sync

Self-host the wrapper data to ensure zero-latency lookups, complete offline capability, and highly customized API integrations.

Step 1: Download Enterprise Catalog SDK

Download the Enterprise-only ZIP file containing compressed catalog metadata. This includes categories, use cases, wrapper codes, input schemas, models, and relations — not prompt logic. Unzip the zywrap-data.json file from the bundle to use in the import script.

Login to check Enterprise Catalog SDK access.

Step 2: Database Setup

Run this SQL. It now features the relational use_cases table containing the dynamic schema_data.

sql

-- Database Schema for Zywrap Enterprise Catalog SDK (MySQL/MariaDB)

CREATE TABLE `ai_models` (
  `code` varchar(255) NOT NULL,
  `name` varchar(255) NOT NULL,
  `status` tinyint(1) DEFAULT 1,
  `ordering` int(11) DEFAULT NULL,
  PRIMARY KEY (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `categories` (
  `code` varchar(255) NOT NULL,
  `name` varchar(255) NOT NULL,
  `status` tinyint(1) DEFAULT 1,
  `ordering` int(11) DEFAULT NULL,
  PRIMARY KEY (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `languages` (
  `code` varchar(10) NOT NULL,
  `name` varchar(255) NOT NULL,
  `status` tinyint(1) DEFAULT 1,
  `ordering` int(11) DEFAULT NULL,
  PRIMARY KEY (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `use_cases` (
  `code` varchar(255) NOT NULL,
  `name` varchar(255) NOT NULL,
  `description` text,
  `category_code` varchar(255) DEFAULT NULL,
  `schema_data` json DEFAULT NULL,
  `status` tinyint(1) DEFAULT 1,
  `ordering` bigint DEFAULT NULL,
  PRIMARY KEY (`code`),
  KEY `category_code` (`category_code`),
  CONSTRAINT `use_cases_ibfk_1` FOREIGN KEY (`category_code`) REFERENCES `categories` (`code`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `wrappers` (
  `code` varchar(255) NOT NULL,
  `name` varchar(255) NOT NULL,
  `description` text,
  `use_case_code` varchar(255) DEFAULT NULL,
  `featured` tinyint(1) DEFAULT NULL,
  `base` tinyint(1) DEFAULT NULL,
  `status` tinyint(1) DEFAULT 1,
  `ordering` bigint DEFAULT NULL,
  PRIMARY KEY (`code`),
  KEY `use_case_code` (`use_case_code`),
  CONSTRAINT `wrappers_ibfk_1` FOREIGN KEY (`use_case_code`) REFERENCES `use_cases` (`code`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `block_templates` (
  `type` varchar(50) NOT NULL,
  `code` varchar(255) NOT NULL,
  `name` varchar(255) NOT NULL,
  `status` tinyint(1) DEFAULT 1,
  PRIMARY KEY (`type`,`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `settings` (
  `setting_key` VARCHAR(255) NOT NULL,
  `setting_value` TEXT,
  PRIMARY KEY (`setting_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `usage_logs` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `trace_id` varchar(255) DEFAULT NULL,
  `wrapper_code` varchar(255) DEFAULT NULL,
  `model_code` varchar(255) DEFAULT NULL,
  `prompt_tokens` int(11) DEFAULT 0,
  `completion_tokens` int(11) DEFAULT 0,
  `total_tokens` int(11) DEFAULT 0,
  `credits_used` bigint DEFAULT 0,
  `latency_ms` int(11) DEFAULT 0,
  `status` varchar(50) DEFAULT 'success',
  `error_message` text,
  `created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `wrapper_idx` (`wrapper_code`),
  KEY `model_idx` (`model_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Step 3: Database Connection

Save this as db.php. It will be used by all other scripts.

php

<?php
// FILE: db.php
// Replace with your actual database credentials
$host = 'localhost';
$db   = 'zywrap_db';
$user = 'root';
$pass = 'password';
$charset = 'utf8mb4';

$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$options = [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => false,
];

try {
     $pdo = new PDO($dsn, $user, $pass, $options);
} catch (\PDOException $e) {
     throw new \PDOException($e->getMessage(), (int)$e->getCode());
}
?>

Step 4: Import Tabular Data

This script uses a custom extractTabular() function to instantly parse and import the compressed Enterprise catalog JSON.

php

<?php
// FILE: import.php
/**
 * Zywrap Enterprise Catalog SDK - Tabular Data Importer
 * USAGE: php import.php
 */
ini_set('max_execution_time', '300');
ini_set('memory_limit', '512M');
require_once 'db.php';

$jsonFile = 'zywrap-data.json';
if (!file_exists($jsonFile)) die("Error: Could not find 'zywrap-data.json'.\n");

$data = json_decode(file_get_contents($jsonFile), true);
if (!$data) die("Error: Could not parse JSON data.");

// Helper to expand tabular JSON data into associative arrays
function extractTabular($tabularData) {
    if (empty($tabularData['cols']) || empty($tabularData['data'])) return [];
    $cols = $tabularData['cols'];
    $result = [];
    foreach ($tabularData['data'] as $row) {
        $result[] = array_combine($cols, $row);
    }
    return $result;
}

try {
    echo "Starting full Enterprise catalog import...\n";
    
    $pdo->exec('SET FOREIGN_KEY_CHECKS = 0;');
    $pdo->exec('TRUNCATE TABLE wrappers; TRUNCATE TABLE use_cases; TRUNCATE TABLE categories;');
    $pdo->exec('TRUNCATE TABLE languages; TRUNCATE TABLE block_templates;');
    $pdo->exec('TRUNCATE TABLE ai_models; TRUNCATE TABLE settings;');
    $pdo->exec('SET FOREIGN_KEY_CHECKS = 1;');

    // START TRANSACTION (Makes imports 100x faster)
    $pdo->beginTransaction();
    echo "Clearing tables...\n";

    // 1. Categories
    if (isset($data['categories'])) {
        $stmt = $pdo->prepare("INSERT INTO categories (code, name, status, ordering) VALUES (?, ?, 1, ?)");
        foreach (extractTabular($data['categories']) as $c) {
            $stmt->execute([$c['code'], $c['name'], $c['ordering'] ?? 99999]);
        }
        echo "Categories imported successfully.\n";
    }
    
    // 2. Use Cases 
    if (isset($data['useCases'])) {
        $stmt = $pdo->prepare("INSERT INTO use_cases (code, name, description, category_code, schema_data, status, ordering) VALUES (?, ?, ?, ?, ?, 1, ?)");
        foreach (extractTabular($data['useCases']) as $uc) {
            $schemaJson = !empty($uc['schema']) ? json_encode($uc['schema']) : null;
            $stmt->execute([$uc['code'], $uc['name'], $uc['desc'], $uc['cat'], $schemaJson, $uc['ordering'] ?? 999999999]);
        }
        echo "Use Cases imported successfully.\n";
    }

    // 3. Wrappers
    if (isset($data['wrappers'])) {
        $stmt = $pdo->prepare("INSERT INTO wrappers (code, name, description, use_case_code, featured, base, status, ordering) VALUES (?, ?, ?, ?, ?, ?, 1, ?)");
        foreach (extractTabular($data['wrappers']) as $w) {
            $featured = !empty($w['featured']) ? 1 : 0;
            $base = !empty($w['base']) ? 1 : 0;
            $stmt->execute([$w['code'], $w['name'], $w['desc'], $w['usecase'], $featured, $base, $w['ordering'] ?? 999999999]);
        }
        echo "Wrappers imported successfully.\n";
    }

    // 4. Languages
    if (isset($data['languages'])) {
        $stmt = $pdo->prepare("INSERT INTO languages (code, name, status, ordering) VALUES (?, ?, 1, ?)");
        $ord = 1;
        foreach (extractTabular($data['languages']) as $l) {
            $stmt->execute([$l['code'], $l['name'], $ord++]);
        }
        echo "Languages imported successfully.\n";
    }
    
    // 5. AI Models
    if (isset($data['aiModels'])) {
        $stmt = $pdo->prepare("INSERT INTO ai_models (code, name, status, ordering) VALUES (?, ?, 1, ?)");
        foreach (extractTabular($data['aiModels']) as $m) {
            $stmt->execute([$m['code'], $m['name'], $m['ordering'] ?? 99999]);
        }
        echo "AI Models imported successfully.\n";
    }

    // 6. Block Templates 
    if (isset($data['templates'])) {
        $stmt = $pdo->prepare("INSERT INTO block_templates (type, code, name, status) VALUES (?, ?, ?, 1)");
        foreach ($data['templates'] as $type => $tabular) {
            foreach (extractTabular($tabular) as $tpl) {
                $stmt->execute([$type, $tpl['code'], $tpl['name']]);
            }
        }
        echo "Block templates imported successfully.\n";
    }

    // 7. Save Version
    if (isset($data['version'])) {
        $stmt = $pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES ('data_version', ?) ON DUPLICATE KEY UPDATE setting_value = ?");
        $stmt->execute([$data['version'], $data['version']]);
        echo "Data version saved to settings table.\n";
    }
    
    // COMMIT TRANSACTION (Saves everything to hard drive instantly)
    $pdo->commit();

    echo "\nāœ… Enterprise catalog import complete! Version: " . ($data['version'] ?? 'N/A') . "\n";

} catch (PDOException $e) {
    // If anything fails, undo all changes
    if ($pdo->inTransaction()) {
        $pdo->rollBack();
    }
    die("Database error during import: " . $e->getMessage() . "\n");
}
?>

Step 5: The Dynamic Playground

This brand new UI automatically renders "Core Inputs" and "Additional Context" fields based on the selected AI Template's schema.

Frontend: playground.html

html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Zywrap Enterprise Catalog Playground</title>
    <style>
        :root {
            --primary: #2563eb; --primary-hover: #1d4ed8;
            --bg: #f8fafc; --card-bg: #ffffff;
            --text-main: #0f172a; --text-muted: #64748b;
            --border: #e2e8f0; --radius: 0.75rem;
        }
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body { font-family: system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--text-main); line-height: 1.5; padding: 2rem; }
        .container { max-width: 1200px; margin: 0 auto; display: grid; grid-template-columns: 1fr 1.8fr; gap: 2rem; }
        @media (max-width: 768px) { .container { grid-template-columns: 1fr; } }
        
        .card { background: var(--card-bg); border-radius: var(--radius); border: 1px solid var(--border); box-shadow: 0 1px 3px rgba(0,0,0,0.05); padding: 1.5rem; margin-bottom: 1.5rem; }
        .card-title { font-size: 1.25rem; font-weight: 700; margin-bottom: 1rem; color: var(--text-main); }
        
        .form-group { margin-bottom: 1rem; }
        label { display: block; font-size: 0.875rem; font-weight: 600; margin-bottom: 0.35rem; color: #334155; }
        
        .input, select, textarea { 
            width: 100%; padding: 0.625rem 0.75rem; border: 1px solid var(--border); 
            border-radius: 0.5rem; font-size: 0.875rem; background: #fff; transition: border-color 0.2s; 
        }
        .input:focus, select:focus, textarea:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }
        textarea { resize: vertical; min-height: 80px; }
        
        .filters { display: flex; gap: 1rem; margin-top: 0.5rem; }
        .filter-label { font-size: 0.875rem; font-weight: normal; display: flex; align-items: center; gap: 0.3rem; color: var(--text-muted); cursor: pointer; }
        
        .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; }
        
        .btn { 
            width: 100%; background: var(--primary); color: white; border: none; padding: 0.875rem; 
            border-radius: 0.5rem; font-weight: 600; font-size: 1rem; cursor: pointer; transition: background 0.2s; 
        }
        .btn:hover { background: var(--primary-hover); }
        .btn:disabled { opacity: 0.7; cursor: not-allowed; }
        
        .schema-section { background: #f8fafc; border: 1px solid var(--border); border-radius: 0.5rem; padding: 1rem; margin-bottom: 1rem; }
        .schema-title { font-size: 0.875rem; font-weight: 700; border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; margin-bottom: 0.75rem; color: #475569; }
        
        #output { background: #f1f5f9; padding: 1.25rem; border-radius: 0.5rem; font-family: ui-monospace, monospace; font-size: 0.875rem; white-space: pre-wrap; min-height: 300px; border: 1px solid var(--border); overflow-x: auto; }
    </style>
</head>
<body>
    <div class="container">
        <div>
            <div class="card">
                <h2 class="card-title">Configuration</h2>
                
                <div class="form-group">
                    <label>1. Category</label>
                    <select id="category"><option value="">Loading...</option></select>
                </div>
                
                <div class="form-group">
                    <label>2. AI Solution</label>
                    <select id="usecase" disabled><option value="">-- Select Category First --</option></select>
                </div>

                <div class="form-group">
                    <label>3. Configuration Style</label>
                    <select id="wrapper" disabled><option value="">-- Select Solution First --</option></select>
                </div>

                <div class="form-group">
                    <label>4. AI Model</label>
                    <select id="model"><option value="">Loading...</option></select>
                </div>

                <div class="form-group">
                    <label>5. Target Language</label>
                    <select id="language"><option value="">Loading...</option></select>
                </div>
            </div>

            <div class="card">
                <h2 class="card-title" style="font-size: 1rem;">Advanced Overrides</h2>
                <div class="grid-2 overrides">
                    <div><label>Tone</label><select id="toneCode"><option value="">-- Default --</option></select></div>
                    <div><label>Style</label><select id="styleCode"><option value="">-- Default --</option></select></div>
                    <div><label>Formatting</label><select id="formatCode"><option value="">-- Default --</option></select></div>
                    <div><label>Complexity</label><select id="complexityCode"><option value="">-- Default --</option></select></div>
                    <div><label>Length</label><select id="lengthCode"><option value="">-- Default --</option></select></div>
                    <div><label>Audience</label><select id="audienceCode"><option value="">-- Default --</option></select></div>
                    <div><label>Goal</label><select id="responseGoalCode"><option value="">-- Default --</option></select></div>
                    <div><label>Output Type</label><select id="outputCode"><option value="">-- Default --</option></select></div>
                </div>
            </div>
        </div>

        <div>
            <div class="card">
                <div id="dynamic-schema-container"></div>
                
                <div class="form-group">
                    <label id="prompt-label">Prompt / Additional Context</label>
                    <textarea id="prompt" class="input" placeholder="Type your request or additional instructions here..."></textarea>
                </div>
                
                <button id="run-button" class="btn">Generate Response</button>
            </div>

            <div class="card">
                <h2 class="card-title">AI Response</h2>
                <pre id="output">Output will appear here...</pre>
            </div>
        </div>
    </div>

    <script>
        const API_ENDPOINT = 'api.php';
        
        const elements = {
            category: document.getElementById('category'),
            usecase: document.getElementById('usecase'), // šŸš€ NEW
            wrapper: document.getElementById('wrapper'),
            model: document.getElementById('model'),
            language: document.getElementById('language'),
            schemaContainer: document.getElementById('dynamic-schema-container'),
            prompt: document.getElementById('prompt'),
            promptLabel: document.getElementById('prompt-label'),
            runBtn: document.getElementById('run-button'),
            output: document.getElementById('output')
        };

        async function fetchAPI(action, params = '') {
            const res = await fetch(`${API_ENDPOINT}?action=${action}&${params}`);
            return await res.json();
        }

        function populateSelect(el, data, placeholder = '-- Select --') {
            el.innerHTML = `<option value="">${placeholder}</option>`;
            data.forEach(item => {
                const opt = document.createElement('option');
                opt.value = item.code; 
                opt.textContent = item.name;
                el.appendChild(opt);
            });
            el.disabled = false;
        }

        async function init() {
            const [categories, models, langs, templates] = await Promise.all([
                fetchAPI('get_categories'),
                fetchAPI('get_ai_models'),
                fetchAPI('get_languages'),
                fetchAPI('get_block_templates')
            ]);
            
            populateSelect(elements.category, categories);
            populateSelect(elements.model, models, 'Default Model');
            populateSelect(elements.language, langs, 'English (Default)');

            const overrideMap = {
                tones: 'toneCode', styles: 'styleCode', formattings: 'formatCode',
                complexities: 'complexityCode', lengths: 'lengthCode', audienceLevels: 'audienceCode',
                responseGoals: 'responseGoalCode', outputTypes: 'outputCode'
            };
            for (const [type, elId] of Object.entries(overrideMap)) {
                if (templates[type]) populateSelect(document.getElementById(elId), templates[type], '-- Default --');
            }
        }

        // šŸš€ CASCADING LOGIC 1: Category -> Load Use Cases
        elements.category.addEventListener('change', async () => {
            if (!elements.category.value) {
                elements.usecase.innerHTML = '<option value="">-- Select Category First --</option>';
                elements.usecase.disabled = true;
                elements.wrapper.innerHTML = '<option value="">-- Select Solution First --</option>';
                elements.wrapper.disabled = true;
                elements.schemaContainer.innerHTML = '';
                return;
            }
            elements.usecase.innerHTML = '<option value="">Loading...</option>';
            const useCases = await fetchAPI('get_use_cases', 'category=' + elements.category.value);
            populateSelect(elements.usecase, useCases, '-- Select a Solution --');
            
            // Reset wrapper
            elements.wrapper.innerHTML = '<option value="">-- Select Solution First --</option>';
            elements.wrapper.disabled = true;
            elements.schemaContainer.innerHTML = '';
        });

        // šŸš€ CASCADING LOGIC 2: Use Case -> Load Wrappers
        elements.usecase.addEventListener('change', async () => {
            if (!elements.usecase.value) {
                elements.wrapper.innerHTML = '<option value="">-- Select Solution First --</option>';
                elements.wrapper.disabled = true;
                elements.schemaContainer.innerHTML = '';
                return;
            }
            
            elements.wrapper.innerHTML = '<option value="">Loading...</option>';
            let wrappers = await fetchAPI('get_wrappers', 'usecase=' + elements.usecase.value);
            
            // Format the labels cleanly (Base vs Variation)
            elements.wrapper.innerHTML = '<option value="">-- Select a Style --</option>';
            
            let autoSelectCode = null; // Track which wrapper to auto-select

            wrappers.forEach((w, index) => {
                const opt = document.createElement('option');
                opt.value = w.code;
                const parts = w.name.split('—');
                const displayName = w.base ? `✨ Base Template - ${parts[0].trim()}` : `↳ Variation: ${parts.length > 1 ? parts[1].trim() : w.name}`;
                opt.textContent = displayName;
                elements.wrapper.appendChild(opt);

                // Identify the Base template, or fallback to the first available variation
                if (w.base) {
                    autoSelectCode = w.code;
                } else if (index === 0 && !autoSelectCode) {
                    autoSelectCode = w.code;
                }
            });
            elements.wrapper.disabled = false;

            // šŸš€ NEW: Auto-select and trigger schema load
            if (autoSelectCode) {
                elements.wrapper.value = autoSelectCode;
                elements.wrapper.dispatchEvent(new Event('change')); // Force the schema to load immediately
            }
        });

        // šŸš€ CASCADING LOGIC 3: Wrapper -> Load Schema
        elements.wrapper.addEventListener('change', async () => {
            elements.schemaContainer.innerHTML = '';
            elements.promptLabel.textContent = "Prompt / Additional Context";
            if (!elements.wrapper.value) return;

            const schema = await fetchAPI('get_schema', 'wrapper=' + elements.wrapper.value);
            if (!schema || (!schema.req && !schema.opt)) return;

            let html = '';
            elements.promptLabel.textContent = "Additional Free-form Instructions";

            const buildSection = (title, data) => {
                if (!data || Object.keys(data).length === 0) return '';
                let sectionHtml = `<div class="schema-section"><h3 class="schema-title">${title}</h3><div class="grid-2">`;
                for (const [key, def] of Object.entries(data)) {
                    const isPlaceholder = def.p !== undefined ? def.p : false;
                    const defaultVal = def.d !== undefined ? def.d : '';
                    const placeholderAttr = isPlaceholder ? `placeholder="${defaultVal}"` : '';
                    const valueAttr = (!isPlaceholder && defaultVal) ? `value="${defaultVal}"` : '';
                    const label = key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
                    
                    sectionHtml += `<div>
                        <label>${label}</label>
                        <input type="text" class="input schema-input" data-key="${key}" ${placeholderAttr} ${valueAttr}>
                    </div>`;
                }
                return sectionHtml + '</div></div>';
            };

            html += buildSection('Core Inputs', schema.req);
            html += buildSection('Additional Context', schema.opt);
            elements.schemaContainer.innerHTML = html;
        });

        // Execute
        elements.runBtn.addEventListener('click', async () => {
            if (!elements.wrapper.value) return alert("Please select a wrapper.");
            elements.output.textContent = 'Executing...';
            elements.runBtn.disabled = true;
            elements.runBtn.textContent = 'Generating...';
            
            // 1. Grab the exact user prompt
            let finalPrompt = elements.prompt.value.trim();
            
            // 2. Build the variables object natively
            const variables = {};
            document.querySelectorAll('.schema-input').forEach(input => {
                const val = input.value.trim();
                if (val !== '') {
                    variables[input.dataset.key] = val;
                }
            });

            // 3. Build overrides
            const overrides = {};
            document.querySelectorAll('.overrides select').forEach(sel => {
                if (sel.value) overrides[sel.id] = sel.value;
            });

            try {
                const response = await fetch(API_ENDPOINT + '?action=execute', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({
                        model: elements.model.value,
                        wrapperCode: elements.wrapper.value,
                        language: elements.language.value,
                        prompt: finalPrompt, // Only the raw text area
                        variables: variables, // Only the object (backend formats it!)
                        overrides: overrides
                    })
                });
                
                const text = await response.text();
                if (!response.ok) {
                    let errMsg = text;
                    try { errMsg = JSON.parse(text).error || errMsg; } catch(e) {}
                    throw new Error(errMsg);
                }
                
                const data = JSON.parse(text);
                elements.output.textContent = data.output || JSON.stringify(data, null, 2);
            } catch (error) {
                elements.output.textContent = 'Error: ' + error.message;
            } finally {
                elements.runBtn.disabled = false;
                elements.runBtn.textContent = 'Generate Response';
            }
        });

        init();
    </script>
</body>
</html>

Backend: api.php

php

<?php
// FILE: api.php
ini_set('max_execution_time', '300');
require 'db.php';
header('Content-Type: application/json');

$zywrapApiKey = "YOUR_ZYWRAP_API_KEY";

// --- Enterprise catalog backend logic ---

function getCategories($pdo) {
    return $pdo->query("SELECT code, name FROM categories WHERE status = 1 ORDER BY ordering ASC")->fetchAll();
}

// šŸš€ NEW: Fetch Solutions (Use Cases) by Category
function getUseCasesByCategory($pdo, $categoryCode) {
    $stmt = $pdo->prepare("
        SELECT code, name 
        FROM use_cases 
        WHERE category_code = ? AND status = 1 
        ORDER BY ordering ASC
    ");
    $stmt->execute([$categoryCode]);
    return $stmt->fetchAll();
}

// šŸš€ UPDATED: Fetch Wrappers (Styles) by Use Case
function getWrappersByUseCase($pdo, $useCaseCode) {
    $stmt = $pdo->prepare("
        SELECT code, name, featured, base 
        FROM wrappers 
        WHERE use_case_code = ? AND status = 1
        ORDER BY ordering ASC
    ");
    $stmt->execute([$useCaseCode]);
    return $stmt->fetchAll();
}

function getSchemaByWrapper($pdo, $wrapperCode) {
    $stmt = $pdo->prepare("
        SELECT uc.schema_data 
        FROM use_cases uc 
        JOIN wrappers w ON w.use_case_code = uc.code 
        WHERE w.code = ?
    ");
    $stmt->execute([$wrapperCode]);
    $result = $stmt->fetchColumn();
    return $result ? json_decode($result, true) : null;
}

function getLanguages($pdo) { 
    return $pdo->query("SELECT code, name FROM languages WHERE status = 1 ORDER BY ordering ASC")->fetchAll(); 
}

function getAiModels($pdo) { 
    return $pdo->query("SELECT code, name FROM ai_models WHERE status = 1 ORDER BY ordering ASC")->fetchAll(); 
}

function getBlockTemplates($pdo) {
    $stmt = $pdo->query("SELECT type, code, name FROM block_templates WHERE status = 1 ORDER BY type, name ASC");
    $grouped = [];
    foreach ($stmt->fetchAll() as $row) {
        $grouped[$row['type']][] = ['code' => $row['code'], 'name' => $row['name']];
    }
    return $grouped;
}

// --- Execution ---
function executeZywrapProxy($apiKey, $model, $wrapperCode, $prompt, $language, $variables, $overrides) {
    $url = 'https://api.zywrap.com/v1/proxy'; 
    
    $payloadData = [
        'model' => $model, 
        'wrapperCodes' => [$wrapperCode], 
        'prompt' => $prompt,
        'variables' => $variables,
        'source' => 'php_sdk'
    ];
    if (!empty($language)) $payloadData['language'] = $language;
    if (!empty($overrides)) $payloadData = array_merge($payloadData, $overrides);

    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payloadData));
    curl_setopt($ch, CURLOPT_TIMEOUT, 300); 
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Content-Type: application/json',
        'Authorization: Bearer ' . $apiKey
    ]);

    $rawResponse = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($httpCode === 200) {
        $lines = explode("\n", $rawResponse);
        $finalJson = null;
        foreach ($lines as $line) {
            $line = trim($line);
            if (strpos($line, 'data: ') === 0) {
                $data = json_decode(substr($line, 6), true);
                if ($data && (isset($data['output']) || isset($data['error']))) $finalJson = substr($line, 6);
            }
        }
        
        $statusCode = 200;
        if ($finalJson) {
            $parsed = json_decode($finalJson, true);
            if (isset($parsed['error'])) $statusCode = 400;
        }
        
        return ['status' => $statusCode, 'response' => $finalJson ?: json_encode(['error' => 'Stream parse failed'])];
    }
    return ['status' => $httpCode, 'response' => $rawResponse];
}

// --- API Router ---
$action = $_GET['action'] ?? '';

switch ($action) {
    case 'get_categories': echo json_encode(getCategories($pdo)); break;
    case 'get_use_cases': echo json_encode(getUseCasesByCategory($pdo, $_GET['category'] ?? '')); break; // šŸš€ NEW
    case 'get_wrappers': echo json_encode(getWrappersByUseCase($pdo, $_GET['usecase'] ?? '')); break; // šŸš€ UPDATED
    case 'get_schema': echo json_encode(getSchemaByWrapper($pdo, $_GET['wrapper'] ?? '')); break;
    case 'get_languages': echo json_encode(getLanguages($pdo)); break;
    case 'get_ai_models': echo json_encode(getAiModels($pdo)); break;
    case 'get_block_templates': echo json_encode(getBlockTemplates($pdo)); break;
    
    case 'execute':
        $input = json_decode(file_get_contents('php://input'), true);
        
        $startTime = microtime(true);
        $result = executeZywrapProxy(
            $zywrapApiKey, 
            $input['model'] ?? null, 
            $input['wrapperCode'] ?? '',
            $input['prompt'] ?? '', 
            $input['language'] ?? null, 
            $input['variables'] ?? [],
            $input['overrides'] ?? []
        );
        $latencyMs = round((microtime(true) - $startTime) * 1000);

        // --- LOGGING ---
        try {
            $status = $result['status'] === 200 ? 'success' : 'error';
            $responseData = json_decode($result['response'], true);
            
            $traceId = $responseData['id'] ?? null;
            $promptTokens = $responseData['usage']['prompt_tokens'] ?? 0;
            $completionTokens = $responseData['usage']['completion_tokens'] ?? 0;
            $totalTokens = $responseData['usage']['total_tokens'] ?? 0;
            $creditsUsed = $responseData['cost']['credits_used'] ?? 0;
            
            $fallbackError = substr($result['response'], 0, 255) . (strlen($result['response']) > 255 ? '...' : '');
            $errorMessage = $status === 'error' ? ($responseData['error'] ?? $fallbackError) : null;

            $stmt = $pdo->prepare("INSERT INTO usage_logs (trace_id, wrapper_code, model_code, prompt_tokens, completion_tokens, total_tokens, credits_used, latency_ms, status, error_message) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
            $stmt->execute([$traceId, $input['wrapperCode'], $input['model'] ?? 'default', $promptTokens, $completionTokens, $totalTokens, $creditsUsed, $latencyMs, $status, $errorMessage]);
        } catch (Exception $e) {
            error_log("Failed to write to usage_logs: " . $e->getMessage());
        }

        http_response_code($result['status']);
        echo $result['response'];
        break;
}
?>

Step 6: Synchronize Your Data

Set this script to run on a cron job. It points to the new /v1/sync endpoint.

php

<?php
// FILE: zywrap-sync.php
/**
 * Zywrap Enterprise Catalog SDK - Smart Data Synchronizer
 *
 * STRATEGY: "Download & Reconcile"
 * 1. Downloads the latest full data bundle (zip) if FULL_RESET.
 * 2. Updates existing records and inserts new ones if DELTA_UPDATE.
 *
 * USAGE: php zywrap-sync.php
 */

// Increase execution time and memory limit for large data processing
ini_set('max_execution_time', '300');
ini_set('memory_limit', '512M'); // āœ… FIX: Added limit

// --- CONFIGURATION ---
$apiKey = "YOUR_ZYWRAP_API_KEY"; 
$apiUrl = 'https://api.zywrap.com/v1/sdk/v1/sync'; // Enterprise catalog sync endpoint
require_once 'db.php'; 

// --- HELPER: UPSERT BATCH (For Delta Updates) ---
function upsertBatch(PDO $pdo, string $tableName, array $rows, array $columns, string $pk = 'code') {
    if (empty($rows)) return;
    
    $colList = implode(", ", $columns);
    $placeholders = implode(", ", array_fill(0, count($columns), "?"));
    
    $updateClause = [];
    foreach ($columns as $col) {
        if ($col !== $pk && $col !== 'type') { 
            $updateClause[] = "$col = VALUES($col)";
        }
    }
    $updateSql = implode(", ", $updateClause);
    $sql = "INSERT INTO $tableName ($colList) VALUES ($placeholders) ON DUPLICATE KEY UPDATE $updateSql";
    
    $stmt = $pdo->prepare($sql);
    $pdo->beginTransaction();
    try {
        foreach ($rows as $row) $stmt->execute($row);
        $pdo->commit();
        echo "   [+] Upserted " . count($rows) . " records into '$tableName'.\n";
    } catch (Exception $e) {
        $pdo->rollBack();
        echo "   [!] Error upserting $tableName: " . $e->getMessage() . "\n";
    }
}

// --- HELPER: DELETE BATCH ---
function deleteBatch(PDO $pdo, string $tableName, array $ids, string $pk = 'code') {
    if (empty($ids)) return;
    $pdo->beginTransaction();
    try {
        $stmt = $pdo->prepare("DELETE FROM $tableName WHERE $pk = ?");
        foreach ($ids as $id) $stmt->execute([$id]);
        $pdo->commit();
        echo "   [-] Deleted " . count($ids) . " records from '$tableName'.\n";
    } catch (Exception $e) {
        $pdo->rollBack();
        echo "   [!] Error deleting from $tableName: " . $e->getMessage() . "\n";
    }
}

// =================================================================
// MAIN LOGIC
// =================================================================

echo "--- šŸš€ Starting Zywrap Enterprise Catalog Sync ---\n";

$stmt = $pdo->query("SELECT setting_value FROM settings WHERE setting_key = 'data_version'");
$localVersion = $stmt->fetchColumn() ?: '';
echo "šŸ”¹ Local Version: " . ($localVersion ?: 'None') . "\n";

// Call API
$url = $apiUrl . '?fromVersion=' . urlencode($localVersion);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Accept: application/json',
    'Authorization: Bearer ' . $apiKey
]);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($httpCode !== 200 || !$response) die("āŒ API Error ($httpCode): $response\n");
$json = json_decode($response, true);
if (!$json) die("āŒ Invalid JSON response.\n");

$mode = $json['mode'] ?? 'UNKNOWN';
echo "šŸ”¹ Sync Mode: $mode\n";

// --- SCENARIO A: FULL RESET ---
if ($mode === 'FULL_RESET') {
    // 🟢 1. Save it with the official name so the user recognizes it
    $zipFile = 'zywrap-data.zip'; 
    $downloadUrl = $json['wrappers']['downloadUrl'];

    echo "ā¬‡ļø  Attempting automatic download from Zywrap...\n";
    
    $fp = fopen($zipFile, 'w+');
    $chDl = curl_init($downloadUrl);
    curl_setopt($chDl, CURLOPT_TIMEOUT, 300);
    curl_setopt($chDl, CURLOPT_FILE, $fp); 
    curl_setopt($chDl, CURLOPT_FOLLOWLOCATION, true);
    curl_setopt($chDl, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($chDl, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . $apiKey]);
    
    $success = curl_exec($chDl);
    $httpCodeDl = curl_getinfo($chDl, CURLINFO_HTTP_CODE);
    curl_close($chDl);
    fclose($fp);
    
    // 2. Check if the download succeeded
    if ($httpCodeDl === 200 && $success && file_exists($zipFile) && filesize($zipFile) > 0) {
        
        $mbSize = round(filesize($zipFile) / 1024 / 1024, 2);
        echo "āœ… Data bundle downloaded successfully ({$mbSize} MB).\n";

        // 3. Attempt Auto-Unzip
        if (class_exists('ZipArchive')) {
            $zip = new ZipArchive;
            if ($zip->open($zipFile) === TRUE) {
                $zip->extractTo(__DIR__);
                $zip->close();
                echo "āœ… Auto-unzip successful. Running import script...\n";
                @unlink($zipFile); // Clean up the zip file after successful extraction
                include 'import.php'; 
            } else {
                // If it fails due to folder permissions
                echo "āš ļø Failed to auto-unzip (Check directory permissions).\n";
                echo "\nšŸ‘‰ ACTION REQUIRED:\n";
                echo "   1. Please manually unzip 'zywrap-data.zip' in this folder.\n";
                echo "   2. Then run: php import.php\n";
            }
        } else {
            // 🟢 4. The graceful fallback if ZipArchive is missing!
            echo "āš ļø PHP 'ZipArchive' extension is missing. Auto-unzip skipped.\n";
            echo "\nšŸ‘‰ ACTION REQUIRED:\n";
            echo "   1. Please manually extract the 'zywrap-data.zip' file into this folder.\n";
            echo "   2. Once extracted, run this command to update your database:\n";
            echo "      php import.php\n";
        }
    } else {
        echo "āŒ Automatic download failed. HTTP Status: $httpCodeDl\n";
        @unlink($zipFile); // Clean up broken/empty file
    }
}
// --- SCENARIO B: DELTA UPDATE ---
elseif ($mode === 'DELTA_UPDATE') {
    
    // 1. Categories (Prisma uses 'position' or 'displayOrder')
    $rows = [];
    foreach(($json['metadata']['categories']??[]) as $r) {
        $status = (!isset($r['status']) || $r['status']) ? 1 : 0;
        $rows[] = [$r['code'], $r['name'], $status, $r['position'] ?? $r['displayOrder'] ?? $r['ordering'] ?? null];
    }
    upsertBatch($pdo, 'categories', $rows, ['code', 'name', 'status', 'ordering']);

    // 2. Languages (Manually mapped in backend)
    $rows = [];
    foreach(($json['metadata']['languages']??[]) as $r) {
        $status = (!isset($r['status']) || $r['status']) ? 1 : 0;
        $rows[] = [$r['code'], $r['name'], $status, $r['ordering'] ?? null];
    }
    upsertBatch($pdo, 'languages', $rows, ['code', 'name', 'status', 'ordering']);

    // 3. AI Models (Prisma uses 'providerId' and 'displayOrder')
    $rows = [];
    foreach(($json['metadata']['aiModels']??[]) as $r) {
        $status = (!isset($r['status']) || $r['status']) ? 1 : 0;
        $rows[] = [
            $r['code'], 
            $r['name'], 
            $status,
            $r['displayOrder'] ?? $r['ordering'] ?? null
        ];
    }
    upsertBatch($pdo, 'ai_models', $rows, ['code', 'name', 'status', 'ordering']);

    // 4. Templates
    $rows = [];
    if (!empty($json['metadata']['templates'])) {
        foreach ($json['metadata']['templates'] as $type => $items) {
            foreach ($items as $item) {
                $status = (!isset($item['status']) || $item['status']) ? 1 : 0;
                $rows[] = [$type, $item['code'], $item['label'] ?? $item['name'] ?? null, $status];
            }
        }
    }
    upsertBatch($pdo, 'block_templates', $rows, ['type', 'code', 'name', 'status']);

    // 5. Use Cases & Schemas
    if (!empty($json['useCases']['upserts'])) {
        $rows = [];
        foreach($json['useCases']['upserts'] as $uc) {
            $schemaJson = !empty($uc['schema']) ? json_encode($uc['schema']) : null;
            $status = (!isset($uc['status']) || $uc['status']) ? 1 : 0;
            $rows[] = [
                $uc['code'], 
                $uc['name'], 
                $uc['description'] ?? null, 
                $uc['categoryCode'] ?? null, 
                $schemaJson, 
                $status,
                $uc['displayOrder'] ?? $uc['ordering'] ?? null
            ];
        }
        upsertBatch($pdo, 'use_cases', $rows, ['code', 'name', 'description', 'category_code', 'schema_data', 'status', 'ordering']);
    }

    // 6. Wrappers
    if (!empty($json['wrappers']['upserts'])) {
        $rows = [];
        foreach($json['wrappers']['upserts'] as $w) {
            $featured = !empty($w['featured'] ?? $w['isFeatured']) ? 1 : 0;
            $base = !empty($w['base'] ?? $w['isBaseWrapper']) ? 1 : 0;
            $status = (!isset($w['status']) || $w['status']) ? 1 : 0;
            
            $rows[] = [
                $w['code'], 
                $w['name'], 
                $w['description'] ?? null, 
                $w['useCaseCode'] ?? $w['categoryCode'] ?? null, 
                $featured, 
                $base, 
                $status,
                $w['displayOrder'] ?? $w['ordering'] ?? null
            ];
        }
        upsertBatch($pdo, 'wrappers', $rows, ['code', 'name', 'description', 'use_case_code', 'featured', 'base', 'status', 'ordering']);
    }

    // 7. Deletes
    if (!empty($json['wrappers']['deletes'])) deleteBatch($pdo, 'wrappers', $json['wrappers']['deletes']);
    if (!empty($json['useCases']['deletes'])) deleteBatch($pdo, 'use_cases', $json['useCases']['deletes']);

    // 8. Update Version
    if (!empty($json['newVersion'])) {
        $pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES ('data_version', ?) ON DUPLICATE KEY UPDATE setting_value=?")->execute([$json['newVersion'], $json['newVersion']]);
    }
    
    echo "āœ… Delta Sync Complete.\n";
}
?>

Programmatically Download

Use this script to fetch the latest Enterprise catalog bundle automatically.

php

<?php
// FILE: download-bundle.php
// USAGE: php download-bundle.php

$apiKey = 'YOUR_API_KEY';
$apiUrl = 'https://api.zywrap.com/v1/sdk/v1/download'; // Enterprise catalog download endpoint
$outputFile = 'zywrap-data.zip';

echo "Downloading latest Enterprise catalog metadata from Zywrap...\n";

$fp = fopen($outputFile, 'w+');
$ch = curl_init($apiUrl);
curl_setopt($ch, CURLOPT_TIMEOUT, 300);
curl_setopt($ch, CURLOPT_FILE, $fp); // Let cURL write directly to the file
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Add this to prevent SSL errors
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer ' . $apiKey,
]);

// $success will be true/false, NOT the file contents!
$success = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

// Close curl AND close the file pointer to save it to disk!
curl_close($ch);
fclose($fp);

if ($httpCode !== 200 || !$success) {
    @unlink($outputFile); // āœ… FIX: Delete corrupted text file masquerading as ZIP on error
    die("Error: Failed to download file. Status code: {$httpCode}\n");
}

echo "āœ… Sync complete. Data saved to {$outputFile}.\n";
echo "Run 'unzip {$outputFile}' to extract the data, then run 'php import.php'.\n";
?>