One contract for site extensions.
Identity, versions and trust
Each plugin has a namespace/name identifier, an independent semantic package version and the exact API version "1". The eyivo namespace is reserved. Type is system or standard; origin is system, verified or third_party. Only platform-approved system-origin packages are accepted in this release. Packages cannot grant themselves required status or new capabilities.
Installation and configuration
The assigned master owns installation records. Install, enable, disable, uninstall and upgrade run under a site lock and database transaction. Dependencies install first; missing packages, cycles, conflicts and downgrades are rejected. Active dependencies and platform-required packages cannot be disabled or removed. Upgrade hooks receive previous_version and may migrate their namespaced configuration through the API. Configuration uses declared scalar/object schemas plus defaults; unknown keys are rejected. Lifecycle handlers must return an array and must not depend on external side effects.
Settings with a clear scope.
Each site and plugin has its own configuration in the assigned master database. The app panel reads and saves through the authenticated Internal API over WireGuard; it holds no master database credentials. Shared services at plugins.eyivo.com use a separate database for service state.
Declared fields
50 maximum
Object containers and nested properties share the same total.
Complete configuration
32 KB maximum
32,768 compact UTF-8 JSON bytes, including keys, values, and defaults.
Nesting depth
3 levels maximum
Top-level fields start at level one.
The host validates schemas when registering a package and checks complete configuration on every write. Unknown keys are rejected. Panel controls and browser validation do not replace server-side enforcement.
Schemas support strings, integers, booleans, and objects with explicit properties. Arrays and arbitrary maps are not configuration types. String lengths are measured in bytes. Top-level fields start at level one; object containers count toward both the field total and nesting depth.
Saves carry the site plugin revision. A stale revision returns a conflict instead of overwriting newer settings. Reload and review the current values before retrying. Mutations to any plugin on the site change this revision, including disabling, re-enabling, uninstalling, and reinstalling.
View configuration schema limits and encoding rules
{
"fields": 50,
"depth": 3,
"bytes": 32768,
"counting": "Every declared property counts, including object containers. Top-level fields are level one. JSON bytes include recursively merged defaults, keys and values, encoded compactly with UTF-8 characters and slashes unescaped.",
"types": [
"string",
"integer",
"boolean",
"object"
],
"policy": "Objects require explicit properties; unknown keys and arrays are rejected. Strings default to 4096 bytes and support min/max up to 32768 bytes. Integer min/max and scalar enum choices are supported. constructor, prototype and __proto__ are reserved keys."
}
Your pages. Eyivo components.
A plugin owns its page definitions, layout, localized content, forms, and declared actions. The central app renders the definitions with trusted components inside the existing user panel. Clicking a plugin opens its declared entry page, such as configuration or its submission inbox.
-
Declare the page
Register page definitions and configuration bindings in the package. Optional menu links target pages owned by that same plugin.
-
Render in the app
The app checks site access and receives validated JSON definitions and data from the assigned master. No plugin PHP or Blade is transferred to or executed by the app.
-
Execute on the master
Authorized actions go through the Internal API to restricted handlers. The master enforces plugin state, capabilities, schemas, and configuration limits.
A single-level Plugins group
Eligible links appear directly under Plugins for the selected site. Nested submenus are not supported. The group stays hidden when no links are visible. Disabling or uninstalling removes the links and blocks direct page and action access, including requests from already-open tabs. Re-enabling restores eligible links.
Packages without a panel declaration, and disabled packages, open an information view from the plugin list. Disabled packages do not expose their registered panel pages or actions. Human Verification supplies a read-only package page; its security defaults remain platform-controlled.
Site membership and declared actions
Pages and custom actions declare view or manage permission. Current site members can view eligible pages and invoke read-only view actions; manage requires the site owner role. The app derives the trusted actor from the signed-in membership. Built-in configuration save/reset actions require manage; custom handlers use declared parameter and response schemas. Each page allows only its listed action IDs.
Declarations do not grant authority
Page loading is read-only. A page or menu cannot grant unrestricted PHP, database access, arbitrary application routes, or scripts inside the authenticated panel. Existing packages without a panel declaration keep working. Third-party package installation remains disabled.
Supported components
Compose the page using these trusted components. Fields bind to declared configuration or action inputs; previews display plain text and tables are read-only. Raw HTML, arbitrary scripts, expressions, and conditional rendering are not supported.
stackgridcardtextalertfieldbuttonpreviewtablesubmission-listsubmission-detail
View the versioned Panel API contract and limits
{
"api": "2",
"execution": "Plugins own JSON page definitions and restricted master-side handlers. App receives validated localized JSON only; plugin PHP, Blade, JavaScript, CSS, raw HTML and handler paths are not transferred or executed in the panel.",
"capabilities": [
"panel.render",
"panel.action",
"site.settings.read",
"site.settings.write"
],
"components": [
"stack",
"grid",
"card",
"text",
"alert",
"field",
"button",
"preview",
"table",
"submission-list",
"submission-detail"
],
"controls": [
"text",
"textarea",
"number",
"checkbox",
"select"
],
"action_types": [
"configuration.save",
"configuration.reset",
"handler"
],
"permissions": {
"view": [
"owner",
"viewer"
],
"manage": [
"owner"
]
},
"limits": {
"pages": 10,
"menu_links": 10,
"menu_depth": 1,
"actions": 20,
"action_input_bytes": 32768,
"components_per_page": 100,
"component_depth": 8,
"page_file_bytes": 65536,
"response_bytes": 262144,
"table_columns": 10,
"table_rows": 100
},
"configuration": "Configuration.save receives a complete configuration object as input; configuration.reset accepts empty input. Both require manage permission and site.settings.write. All panel actions require panel.action. Custom handlers declare view (read-only even for owners) or manage permission; their input/response use declared bounded schemas. Page loaders are read-only.",
"concurrency": "Every action requires the page revision. The master compares site.plugin_revision under the existing site row lock; stale saves return HTTP 409. Deactivation or uninstall blocks pages/actions and removes derived menus.",
"localization": "Definitions use locale maps with a required en fallback. The master localizes text before sending JSON to app. Field labels and input guidance are required.",
"supported_versions": [
"1",
"2"
],
"submissions": {
"version": "2",
"declaration": "submission-list declares form_type and detail_page; submission-detail declares the same form_type and list_page. Every registered form type requires mutually linked viewable pages and a flat sidebar menu entry to its list.",
"execution": "The host resolves current site/plugin ownership and supplies paginated summaries or a stored historical detail snapshot. App constructs all navigation and action URLs. Package PHP never reaches app.",
"contexts": "Page loaders receive authenticated actor and validated query/record context. Plugin operations cannot override the current site or plugin. Creating records requires an out-of-band host-issued FormProcessingContext unavailable to interpreted package code.",
"state": "Reading, archive and workflow state are independent. Record mutations require the current record_revision; configuration still uses plugin_revision. Viewing never marks a submission read. Viewers may read; existing owner/manage permissions govern mutations."
}
}
Forms and submission history
Forms API 1 provides seven approved processors. Themes own layout and styling; plugins own schemas, validation and workflow. The host handles persistence, verification and notifications.
| System plugin | Form type |
|---|---|
eyivo/contact | Contact contact |
eyivo/quote-request | Quote request quote_request |
eyivo/product-inquiry | Product inquiry product_inquiry |
eyivo/appointment-request | Appointment request appointment |
eyivo/course-application | Course application course_application |
eyivo/reservation-request | Reservation request reservation |
eyivo/property-inquiry | Property inquiry property_inquiry |
One form, reusable placements
Setup creates a retained default UUID per site/plugin/type. Reusing a placement keeps that identity; creating a named form gives a separate UUID. Theme changes and plugin reinstall preserve history. Themes declare portable aliases; the editor selects saved forms without asking customers to type UUIDs.
{
"plugins": ["eyivo/contact", "eyivo/quote-request"],
"forms": {
"contact": {"plugin":"eyivo/contact","type":"contact","schema_version":1,"instance":"default"},
"quote": {"plugin":"eyivo/quote-request","type":"quote_request","schema_version":1,"instance":"default"}
}
}
Use forms.contact in contact templates and component.form in lead sections. Replace form_preset with form_binding. Render supplied fields and guidance, action, CSRF, signed form_context and verification_html. Only declared presentation overrides and permitted additional fields are accepted; required validation cannot be weakened.
forms.contact · component.form · form_binding · form.fields · form.action · form.csrf_token · form.context · form.verification_html
Two inbox views, one record
The shared site inbox and each form plugin’s own list/detail pages show the same records. Form packages require Panel API 2 submission-list/submission-detail components and a flat menu link. Panel API 1 remains supported. Viewing does not mark records read; read/archive/workflow states are separate and edits use record_revision. Disabled plugins lose menus/pages, while the shared inbox retains historical snapshots.
{"components":[{"type":"submission-list","form_type":"contact","detail_page":"submission"}]}
{"components":[{"type":"submission-detail","form_type":"contact","list_page":"submissions"}]}
Customer data and permissions
Form records stay in the assigned site content database and share the existing package max_db_bytes allowance with other content. No separate submission-count or retention quota is added. Configuration remains in the master database with limits of 50 fields, 32 KB and three levels. Members can view; owner/manage permissions govern changes. Plugins receive no direct database access, and PHP/Blade never moves to the app.
Verified saving and notification
The host validates signed context, declared answers and required Human Verification, then commits the record, usage and notification work together. Identical attempts and payloads recover for 24 hours without saving twice. SMTP delivery runs afterward and may retry; an email failure does not undo a saved record. An unset form recipient falls back to the company email; if neither address is valid, the submission is saved without email.
All shipped themes use this contract. Legacy contact POSTs return 410; contact-page GET links still work. Appointment and reservation requests do not confirm bookings. Third-party form processors, file uploads and arbitrary external actions are not included.
POST /__eyivo/forms/{UUID}/submissionsGET /__eyivo/forms/{UUID}/challenge
Themes and form placement
Themes declare approved dependencies and portable forms aliases in theme.json. A binding identifies plugin, type, schema_version and instance; setup resolves it to a retained site form UUID. Contact templates use forms.contact, and lead components use component.form with form_binding. Render the supplied fields, action, CSRF token, signed form_context and verification_html. Use only declared presentation changes or permitted additional fields; required plugin validation cannot be weakened. All shipped themes use this contract. External actions and the retired contact POST are not supported submission paths.
{"component_type":"eyivo:lead_form","data":{"form_binding":"contact"}}
Forms API 1
{
"api": "1",
"approval": "Only configured, approved Eyivo system form packages may declare forms; they require Human Verification and mandatory Panel API 2 submission pages.",
"schema": "forms.types maps stable identifiers to schema_version, localized label, fields, extensions, workflow and a restricted processor file. Fields declare scalar type, compatible control, required flag, localized label and guidance; strings require explicit max. No PHP, SQL, validation expressions or unknown fields are accepted.",
"field_types": [
"string",
"integer",
"boolean"
],
"controls": [
"text",
"textarea",
"email",
"tel",
"url",
"date",
"datetime-local",
"select",
"number",
"checkbox"
],
"presentation_extensions": [
"label",
"placeholder",
"help",
"order"
],
"storage": "The host owns site content storage. Plugin packages receive no credentials, database connection, SQL or unrestricted internal services. Site ownership is injected; schema snapshots remain readable without installed plugin code.",
"quotas": "Submission data consumes the existing package database allowance. Configuration limits of 50 fields / 32768 bytes / three levels do not cap form fields or accumulated submissions. Existing runtime, request and pagination bounds are technical transport protections.",
"identity": "Each site/plugin/type has a retained default form UUID created during authorized setup. Themes resolve portable aliases; rendering does not create instances. Additional named instances receive their own UUID. Theme changes and plugin reinstall preserve history.",
"transport": {
"encoding": "Escaped JSON byte counts match the restricted plugin interpreter; these technical ceilings do not limit accumulated form or submission counts.",
"schema_bytes": 49152,
"schema_accounting": "Definition plus one additional workflow copy, reserving the repeated workflow information on detail responses.",
"answers_bytes": 49152,
"source_bytes": 4096,
"processor_context_bytes": 114688,
"maximum_api_page": 25,
"instances_summary": [
"id",
"plugin_id",
"form_type",
"name",
"schema_version_id",
"record_revision",
"is_default"
]
}
}
Plugin manifest example
{
"id": "eyivo/human-verification",
"name": "Human Verification",
"version": "1.1.0",
"api": "1",
"type": "system",
"origin": "system",
"required": true,
"capabilities": [
"form.inject",
"form.validate",
"frontend.assets",
"public.endpoint",
"verification.challenge",
"verification.verify",
"panel.render"
],
"panel": {
"api": "1",
"entry": "settings",
"pages": {
"settings": {
"title": {
"en": "Human Verification",
"id": "Verifikasi Manusia",
"tr": "\u0130nsan Do\u011frulama",
"de": "Menschliche Verifizierung",
"es": "Verificaci\u00f3n humana",
"fr": "V\u00e9rification humaine",
"pt": "Verifica\u00e7\u00e3o humana",
"it": "Verifica umana",
"ar": "\u0627\u0644\u062a\u062d\u0642\u0642 \u0627\u0644\u0628\u0634\u0631\u064a"
},
"permission": "view",
"view": "panel/settings.json",
"actions": []
}
},
"menu": [],
"actions": []
},
"dependencies": {
"plugins": []
},
"conflicts": [],
"configuration": [],
"defaults": [],
"hooks": {
"form.before_submit": "render.php",
"form.validate": "validate.php"
},
"render_hook": "form.before_submit",
"required_validators": [
"contact"
],
"lifecycle": [],
"assets": [
{
"path": "assets/verification.css",
"type": "style"
},
{
"path": "assets/verification.js",
"type": "script"
}
],
"endpoints": {
"challenge": {
"method": "GET",
"handler": "challenge.php",
"parameters": {
"form_id": {
"type": "string",
"required": true,
"enum": [
"contact"
]
},
"locale": {
"type": "string",
"max": 10
}
},
"response": {
"token": {
"type": "string",
"max": 4096
},
"image": {
"type": "string",
"max": 65536
},
"expires_in": {
"type": "integer"
}
}
}
}
}
System plugin · eyivo/contact · Forms API 1 / Panel API 2
{
"id": "eyivo/contact",
"name": "Contact",
"version": "1.0.0",
"api": "1",
"type": "system",
"origin": "system",
"required": false,
"capabilities": [
"forms.submissions.create",
"forms.submissions.read",
"forms.submissions.manage",
"forms.instances.read",
"forms.instances.manage",
"panel.render",
"panel.action"
],
"dependencies": {
"plugins": [
"eyivo/human-verification"
]
},
"conflicts": [],
"configuration": [],
"defaults": [],
"hooks": [],
"required_validators": [],
"lifecycle": [],
"assets": [],
"endpoints": [],
"forms": {
"api": "1",
"types": {
"contact": {
"schema_version": 1,
"label": {
"en": "Contact",
"id": "Kontak",
"tr": "İletişim",
"de": "Kontakt",
"es": "Contacto",
"fr": "Contact",
"pt": "Contato",
"it": "Contatti",
"ar": "التواصل",
"ru": "Контакт",
"nl": "Contact",
"pl": "Kontakt",
"ja": "お問い合わせ",
"ko": "문의",
"zh": "联系"
},
"fields": {
"name": {
"type": "string",
"control": "text",
"required": true,
"label": {
"en": "Name",
"id": "Nama",
"tr": "Ad",
"de": "Name",
"es": "Nombre",
"fr": "Nom",
"pt": "Nome",
"it": "Nome",
"ar": "الاسم",
"ru": "Имя",
"nl": "Naam",
"pl": "Imię i nazwisko",
"ja": "氏名",
"ko": "이름",
"zh": "姓名"
},
"max": 255,
"placeholder": {
"en": "Your full name",
"id": "Nama lengkap Anda",
"tr": "Adınız ve soyadınız",
"de": "Ihr vollständiger Name",
"es": "Tu nombre completo",
"fr": "Votre nom complet",
"pt": "Seu nome completo",
"it": "Il tuo nome completo",
"ar": "اسمك الكامل",
"ru": "Ваше полное имя",
"nl": "Uw volledige naam",
"pl": "Twoje imię i nazwisko",
"ja": "氏名を入力してください",
"ko": "성명을 입력하세요",
"zh": "请输入您的姓名"
},
"autocomplete": "name"
},
"email": {
"type": "string",
"control": "email",
"required": true,
"label": {
"en": "Email",
"id": "Email",
"tr": "E-posta",
"de": "E-Mail",
"es": "Correo electrónico",
"fr": "E-mail",
"pt": "E-mail",
"it": "Email",
"ar": "البريد الإلكتروني",
"ru": "Электронная почта",
"nl": "E-mail",
"pl": "E-mail",
"ja": "メールアドレス",
"ko": "이메일",
"zh": "电子邮箱"
},
"max": 254,
"placeholder": {
"en": "Your email address",
"id": "Alamat email Anda",
"tr": "E-posta adresiniz",
"de": "Ihre E-Mail-Adresse",
"es": "Tu dirección de correo",
"fr": "Votre adresse e-mail",
"pt": "Seu endereço de e-mail",
"it": "Il tuo indirizzo email",
"ar": "عنوان بريدك الإلكتروني",
"ru": "Ваш адрес электронной почты",
"nl": "Uw e-mailadres",
"pl": "Twój adres e-mail",
"ja": "メールアドレスを入力してください",
"ko": "이메일 주소를 입력하세요",
"zh": "请输入您的电子邮箱地址"
},
"autocomplete": "email"
},
"phone": {
"type": "string",
"control": "tel",
"required": false,
"label": {
"en": "Phone",
"id": "Telepon",
"tr": "Telefon",
"de": "Telefon",
"es": "Teléfono",
"fr": "Téléphone",
"pt": "Telefone",
"it": "Telefono",
"ar": "الهاتف",
"ru": "Телефон",
"nl": "Telefoon",
"pl": "Telefon",
"ja": "電話番号",
"ko": "전화번호",
"zh": "电话"
},
"max": 50,
"placeholder": {
"en": "Country code and phone number",
"id": "Kode negara dan nomor telepon",
"tr": "Ülke kodu ve telefon numarası",
"de": "Ländervorwahl und Telefonnummer",
"es": "Código de país y teléfono",
"fr": "Indicatif du pays et numéro",
"pt": "Código do país e telefone",
"it": "Prefisso internazionale e numero",
"ar": "رمز الدولة ورقم الهاتف",
"ru": "Код страны и номер телефона",
"nl": "Landcode en telefoonnummer",
"pl": "Numer telefonu z kodem kraju",
"ja": "国番号と電話番号を入力してください",
"ko": "국가 코드와 전화번호를 입력하세요",
"zh": "请输入国家代码和电话号码"
},
"autocomplete": "tel"
},
"message": {
"type": "string",
"control": "textarea",
"required": true,
"label": {
"en": "Message",
"id": "Pesan",
"tr": "Mesaj",
"de": "Nachricht",
"es": "Mensaje",
"fr": "Message",
"pt": "Mensagem",
"it": "Messaggio",
"ar": "الرسالة",
"ru": "Сообщение",
"nl": "Bericht",
"pl": "Wiadomość",
"ja": "メッセージ",
"ko": "메시지",
"zh": "留言"
},
"max": 20000,
"placeholder": {
"en": "Describe your question or request",
"id": "Jelaskan pertanyaan atau permintaan Anda",
"tr": "Sorunuzu veya talebinizi açıklayın",
"de": "Beschreiben Sie Ihre Frage oder Anfrage",
"es": "Describe tu pregunta o solicitud",
"fr": "Décrivez votre question ou demande",
"pt": "Descreva sua dúvida ou solicitação",
"it": "Descrivi la tua domanda o richiesta",
"ar": "صف سؤالك أو طلبك",
"ru": "Опишите ваш вопрос или запрос",
"nl": "Beschrijf uw vraag of verzoek",
"pl": "Opisz swoje pytanie lub prośbę",
"ja": "ご質問やご要望を入力してください",
"ko": "질문이나 요청 내용을 설명하세요",
"zh": "请描述您的问题或需求"
}
}
},
"extensions": {
"presentation": [
"label",
"placeholder",
"help",
"order"
],
"field_types": [
"string",
"integer",
"boolean"
]
},
"workflow": {
"initial": "received",
"states": {
"received": {
"en": "Received",
"id": "Diterima",
"tr": "Alındı",
"de": "Eingegangen",
"es": "Recibido",
"fr": "Reçu",
"pt": "Recebido",
"it": "Ricevuto",
"ar": "تم الاستلام"
},
"in_progress": {
"en": "In progress",
"id": "Sedang diproses",
"tr": "İşlemde",
"de": "In Bearbeitung",
"es": "En curso",
"fr": "En cours",
"pt": "Em andamento",
"it": "In corso",
"ar": "قيد المعالجة"
},
"completed": {
"en": "Completed",
"id": "Selesai",
"tr": "Tamamlandı",
"de": "Abgeschlossen",
"es": "Completado",
"fr": "Terminé",
"pt": "Concluído",
"it": "Completato",
"ar": "مكتمل"
}
},
"transitions": {
"start": {
"from": [
"received"
],
"to": "in_progress",
"label": {
"en": "Start processing",
"id": "Mulai proses",
"tr": "İşleme al",
"de": "Bearbeitung starten",
"es": "Iniciar gestión",
"fr": "Commencer le traitement",
"pt": "Iniciar atendimento",
"it": "Avvia elaborazione",
"ar": "بدء المعالجة"
}
},
"complete": {
"from": [
"received",
"in_progress"
],
"to": "completed",
"label": {
"en": "Complete",
"id": "Selesaikan",
"tr": "Tamamla",
"de": "Abschließen",
"es": "Completar",
"fr": "Terminer",
"pt": "Concluir",
"it": "Completa",
"ar": "إكمال"
}
},
"reopen": {
"from": [
"completed"
],
"to": "received",
"label": {
"en": "Reopen",
"id": "Buka kembali",
"tr": "Yeniden aç",
"de": "Erneut öffnen",
"es": "Reabrir",
"fr": "Rouvrir",
"pt": "Reabrir",
"it": "Riapri",
"ar": "إعادة الفتح"
}
}
}
},
"processor": "submit.php"
}
}
},
"panel": {
"api": "2",
"entry": "submissions",
"pages": {
"submissions": {
"title": {
"en": "Contact",
"id": "Kontak",
"tr": "İletişim",
"de": "Kontakt",
"es": "Contacto",
"fr": "Contact",
"pt": "Contato",
"it": "Contatti",
"ar": "التواصل"
},
"permission": "view",
"view": "panel/submissions.json",
"actions": []
},
"submission": {
"title": {
"en": "Submission",
"id": "Kiriman",
"tr": "Form kaydı",
"de": "Einsendung",
"es": "Solicitud",
"fr": "Demande",
"pt": "Solicitação",
"it": "Richiesta",
"ar": "الطلب"
},
"permission": "view",
"view": "panel/submission.json",
"actions": []
}
},
"menu": [
{
"page": "submissions",
"label": {
"en": "Contact",
"id": "Kontak",
"tr": "İletişim",
"de": "Kontakt",
"es": "Contacto",
"fr": "Contact",
"pt": "Contato",
"it": "Contatti",
"ar": "التواصل"
},
"icon": "list",
"order": 100
}
],
"actions": []
}
}
Declared capabilities
Declare the capabilities a package needs. The host checks them before allowing each operation.
frontend.renderfrontend.assetsform.injectform.validatepublic.endpointsite.settings.readsite.settings.writeverification.challengeverification.verifypanel.renderpanel.actionforms.submissions.createforms.submissions.readforms.submissions.manageforms.instances.readforms.instances.manage
Hooks and lifecycle events
Frontend hooks
page.headpage.body.startpage.body.endform.startform.before_fieldsform.after_fieldsform.before_submitform.after_submitform.end
Backend hooks
form.before_validateform.validateform.after_validateform.before_processform.after_process
Lifecycle events
installenabledisableuninstallupgrade
Validation and processing order
The shared form engine validates tenant, signed placement context, current plugin/schema and declared answers before verification. It binds proof recovery to the attempt and canonical payload. After rechecking state and quota, the owning restricted processor saves through the host API in one content transaction with the attempt receipt, usage change and notification work. Identical retries recover the receipt without saving again. Email delivery happens afterward and may retry; mail failure never removes a saved submission.
Restricted PHP runtime
eyivo-php-subset-1
Context and results
Handlers receive $context as JSON-compatible data: site_id, site_public_id, plugin_id, api, configuration, locale and the applicable page/form_id/form_type/action/method/fields/validation_state. Site identity is always supplied by the host. Render hooks return an html field; validation hooks return a boolean valid field. Every required validator must explicitly return true. Other backend hooks return an array. Submitted fields are bounded and contain only the public form fields and proof. Framework objects, secrets, user tokens and database connections are never exposed.
API bridge and permissions
$pluginApi exposes get, post, put, patch and delete with a literal operation ID and a parameter array. Every operation is checked against the manifest for method, capability, request schema, response schema and optional plugin restriction. Unexposed URLs are denied. Site and plugin scope cannot be overridden. Settings operations share the same application service as authenticated Internal API configuration management. Settings writes are limited to authorized lifecycle handlers and owner-authorized manage panel actions. Page loaders, view actions, public endpoints and render hooks cannot alter installation configuration.
View supported syntax and execution limits
{
"execution": "Interpreted PHP subset. Package code is never included, evaluated, or autoloaded by PHP.",
"statements": [
"local assignment",
"return array",
"if / else with braces"
],
"expressions": [
"single-quoted strings",
"integers",
"booleans",
"null",
"arrays",
"array lookup",
"concatenation",
"strict equality",
"strict inequality",
"boolean operators",
"null coalescing"
],
"functions": [
"trim",
"strtolower",
"strlen",
"count",
"htmlspecialchars"
],
"forbidden": [
"all undeclared functions",
"superglobals",
"objects and classes",
"dynamic calls",
"references",
"loops",
"includes",
"eval",
"filesystem access",
"network access",
"database access",
"environment access",
"configuration manipulation",
"deserialization"
],
"source_bytes": 32768,
"tokens": 4096,
"syntax_depth": 32,
"evaluation_steps": 4096,
"api_calls_per_handler": 16,
"handler_wall_seconds": 10,
"binary_expression_chain": 128,
"forms_per_page": 32,
"data_bytes": 131072
}
Assets and public endpoints
Packages register local script, style and image files. Scripts/styles load once when the plugin renders; only registered files are served. Inline plugin HTML uses an element/attribute allowlist. Custom-theme Content Security Policy authorizes the registered script hashes with Subresource Integrity. Endpoints use /__eyivo/plugins/{namespace}/{name}/{endpoint}, explicit methods and declared input fields. Tenant resolution, rate limits and CSRF protection for unsafe methods remain host-controlled. Plugin endpoints never add arbitrary application routes.
Human Verification
eyivo/human-verification is required on every site. The shared service at plugins.eyivo.com/verification uses encrypted six-digit PNG challenges with a 180-second expiry. Trusted client identity, granted services and source IPs are enforced. Registered form UUIDs and attempt IDs bind each challenge; verification also binds the canonical payload. Identical accepted attempts can recover their result for 24 hours, so retain previous keys for that window plus a clock/network margin. Newly expired proofs fail. The shared plugins database stores replay/rate-limit/receipt state, never form answers or inbox history. Packages receive no database access.
Accessibility and browser behavior
The control has localized labels, input guidance, status announcements and an image refresh button. JavaScript must be enabled. This initial image-only mode cannot be completed by visitors unable to perceive the image; an accessible alternative challenge is not part of v1. Pages containing supported forms bypass shared HTML caching. Theme previews keep their existing no-script policy and cannot submit working challenges.
Exposed API operations
Use these operation IDs through $pluginApi. Each entry below comes from the same exposure manifest used to enforce runtime permissions.
site.settings.read
Read the current plugin's namespaced site configuration.
site.settings.read
$pluginApi->get('site.settings.read', []);
View parameters, response, and restrictions
{
"parameters": [],
"response": {
"configuration": {
"type": "object"
}
},
"restricted_to_plugins": null,
"deprecated": false
}
site.settings.update
Replace the current plugin's configuration using its declared schema.
site.settings.write
$pluginApi->put('site.settings.update', ['configuration' => []]);
View parameters, response, and restrictions
{
"parameters": {
"configuration": {
"type": "object",
"required": true
}
},
"response": {
"configuration": {
"type": "object"
}
},
"restricted_to_plugins": null,
"deprecated": false
}
form.verification.widget
Render a localized verification control for a supported form.
form.inject
$pluginApi->get('form.verification.widget', []);
View parameters, response, and restrictions
{
"parameters": [],
"response": {
"html": {
"type": "string",
"max": 16384
}
},
"restricted_to_plugins": [
"eyivo/human-verification"
],
"deprecated": false
}
form.verification.challenge
Request a numeric image challenge bound to the current site and form.
verification.challenge
$pluginApi->get('form.verification.challenge', []);
View parameters, response, and restrictions
{
"parameters": [],
"response": {
"token": {
"type": "string",
"max": 4096
},
"image": {
"type": "string",
"max": 65536
},
"expires_in": {
"type": "integer"
}
},
"restricted_to_plugins": [
"eyivo/human-verification"
],
"deprecated": false
}
form.verification.verify
Consume a proof once. Rejection never allows form processing.
verification.verify
$pluginApi->post('form.verification.verify', ['token' => $context['fields']['verification_token'], 'answer' => $context['fields']['verification_answer']]);
View parameters, response, and restrictions
{
"parameters": {
"token": {
"type": "string",
"required": true,
"max": 4096
},
"answer": {
"type": "string",
"required": true,
"max": 12
}
},
"response": {
"valid": {
"type": "boolean"
}
},
"restricted_to_plugins": [
"eyivo/human-verification"
],
"deprecated": false
}
forms.submissions.create
Persist only the host-validated and verified attempt supplied out-of-band to the owning processor; accepts no answers or ownership override.
forms.submissions.create
$pluginApi->post('forms.submissions.create', []);
View parameters, response, and restrictions
{
"parameters": [],
"response": {
"result": {
"type": "object"
}
},
"restricted_to_plugins": null,
"deprecated": false
}
forms.submissions.list
Read paginated submission summaries for the current site and plugin in an authenticated panel context.
forms.submissions.read
$pluginApi->get('forms.submissions.list', []);
View parameters, response, and restrictions
{
"parameters": {
"form_type": {
"type": "string",
"max": 64
},
"form_id": {
"type": "string",
"max": 36
},
"cursor": {
"type": "string",
"max": 1024
},
"per_page": {
"type": "integer",
"min": 1,
"max": 25
},
"read": {
"type": "boolean"
},
"archived": {
"type": "boolean"
},
"workflow_state": {
"type": "string",
"max": 64
},
"from": {
"type": "string",
"max": 10
},
"to": {
"type": "string",
"max": 10
}
},
"response": {
"result": {
"type": "object"
}
},
"restricted_to_plugins": null,
"deprecated": false
}
forms.submissions.read
Read a historical submission snapshot owned by the current site and plugin.
forms.submissions.read
$pluginApi->get('forms.submissions.read', []);
View parameters, response, and restrictions
{
"parameters": {
"submission_id": {
"type": "string",
"max": 36,
"required": true
}
},
"response": {
"result": {
"type": "object"
}
},
"restricted_to_plugins": null,
"deprecated": false
}
forms.submissions.update
Apply authorized reading/archive changes or a declared workflow transition using the current record revision.
forms.submissions.manage
$pluginApi->patch('forms.submissions.update', []);
View parameters, response, and restrictions
{
"parameters": {
"submission_id": {
"type": "string",
"max": 36,
"required": true
},
"record_revision": {
"type": "integer",
"min": 1,
"required": true
},
"read": {
"type": "boolean"
},
"archived": {
"type": "boolean"
},
"transition": {
"type": "string",
"max": 64
}
},
"response": {
"result": {
"type": "object"
}
},
"restricted_to_plugins": null,
"deprecated": false
}
forms.submissions.delete
Explicitly delete an owned submission, cancel pending notifications and release its stored byte charge; preserve technical replay protection.
forms.submissions.manage
$pluginApi->delete('forms.submissions.delete', []);
View parameters, response, and restrictions
{
"parameters": {
"submission_id": {
"type": "string",
"max": 36,
"required": true
},
"record_revision": {
"type": "integer",
"min": 1,
"required": true
}
},
"response": {
"result": {
"type": "object"
}
},
"restricted_to_plugins": null,
"deprecated": false
}
forms.instances.list
Read cursor-paginated compact form summaries belonging to the current site and plugin; settings are not included.
forms.instances.read
$pluginApi->get('forms.instances.list', []);
View parameters, response, and restrictions
{
"parameters": {
"form_type": {
"type": "string",
"max": 64
},
"cursor": {
"type": "string",
"max": 1024
},
"per_page": {
"type": "integer",
"min": 1,
"max": 25
},
"q": {
"type": "string",
"max": 160
}
},
"response": {
"result": {
"type": "object"
}
},
"restricted_to_plugins": null,
"deprecated": false
}
forms.instances.create
Create a separate named form instance with an automatically generated UUID.
forms.instances.manage
$pluginApi->post('forms.instances.create', []);
View parameters, response, and restrictions
{
"parameters": {
"form_type": {
"type": "string",
"max": 64,
"required": true
},
"name": {
"type": "string",
"max": 160,
"required": true
}
},
"response": {
"result": {
"type": "object"
}
},
"restricted_to_plugins": null,
"deprecated": false
}
forms.instances.update
Update a current form instance name or schema-compatible settings under owner/manage authorization and record revision.
forms.instances.manage
$pluginApi->patch('forms.instances.update', []);
View parameters, response, and restrictions
{
"parameters": {
"form_id": {
"type": "string",
"max": 36,
"required": true
},
"record_revision": {
"type": "integer",
"min": 1,
"required": true
},
"name": {
"type": "string",
"max": 160
},
"settings": {
"type": "object"
}
},
"response": {
"result": {
"type": "object"
}
},
"restricted_to_plugins": null,
"deprecated": false
}
Compatibility and deployment
Plugin API 1 supports Forms API 1 and Panel API 1/2. Form packages require Panel API 2 list/detail pages and menu declarations. Unsupported contracts and unsafe syntax are rejected. Deploy the verification receipt service first, then master/content migrations, plugin reconciliation, legacy migration and usage accounting; deploy the compatible app renderer and Apex reference afterward. Preserve history and old keys during rollback. All shipped themes use registered forms; legacy contact POSTs return 410. The exposure manifest supplies this reference and runtime permissions.