Developers Docs ↗ Developers
Developers

Extension & developer docs

Build your own generators, tools, panels, effects, export formats and more on the public extension API. It is additive and versioned; feature-detect newer points and your extension keeps working across editor versions.

JavaScript APIPHP hooksThe bridgeClient-side

Extension API Developers #

Build your own generators, tools, panels and more.

The editor exposes a public extension API. An extension can add design generators, asset-library sections, starter-template packs, local AI tools (with access to the self-hosted transformer runtime), pixel effects, filter presets, menu items, panels, properties-panel sections, canvas tools, export formats and stock providers, and react to save events, all without touching the plugin’s code.

Start here

Download the extension starter - a buildable package with the canonical register pattern, the UI kit, mounted editor components, per-user storage, i18n and a Playwright smoke test wired up (npm install && npm run build). IntelliSense comes from wpie-extension.d.ts, and manifest.schema.json validates your manifest in any editor via its $schema line.

There are two ways to ship an extension:

  1. Extension package (recommended): a ZIP with a manifest.json and a JavaScript entry file, installed inside the editor under Extensions, then Manage Extensions. It never appears in the WordPress plugin list, loads only on the editor page, and is pure client-side (the archive may not contain PHP).
  2. WordPress plugin: for extensions that also need server-side hooks (stock providers, save events, bootstrap data).

Extension packages Developers #

A package is a ZIP whose top level contains a manifest.json:

{
  "name": "Paper Doodles",
  "slug": "wpie-paper-doodles",
  "version": "1.0.0",
  "author": "Jane Doe",
  "description": "Doodle shapes for the asset library.",
  "main": "extension.js",
  "style": "style.css",
  "requiresApi": "2.0.0",
  "provides": [ "librarySection" ]
}
  • name and main are required; slug defaults to a sanitized name; style is enqueued alongside the script.
  • Allowed file types: JS, JSON, CSS, source maps, images, fonts, text and 3D assets. PHP is rejected, so packages can never run server-side code.
  • Installing needs manage_options; packages install to uploads/wpie-extensions/<slug>/, and re-installing a slug replaces it (that is the update path).
  • category (optional) sorts your package into the Extensions menu. One slug from the curated, host-translated vocabulary: photo (Photo & Effects), motion (Motion & Video), 3d (3D & Scenes), art (Art & Illustration), data (Data & Diagrams), marketing (Marketing & Social), print (Print & Posters), tools (Tools & Workflow). Anything else lands under Other. Items registered via registerMenuItem( 'extensions', … ) declare the same slug on the item: { label, run, category: '3d' }.
  • generatorPrefixes (optional): if your generator ids use a namespace that differs from the package slug (legacy ids live inside saved documents and must never change), list those prefixes so the menu can still attribute your generators.

The entry script registers the same way whether it ships in a package or a plugin:

function register( api ) { /* api.register…() calls */ }
if ( window.WPIE?.api ) {
  register( window.WPIE.api );
} else {
  wp.hooks.addAction( 'wpie.ready', 'my-extension', register );
}

From a WordPress plugin, enqueue on the wpie_editor_assets action (it fires only on the editor page) and depend on wpie-editor and wp-hooks.

Localizing a pack

The editor text domain only covers core strings; packs ship their own translations. Read the editor user’s locale from window.WPIE.locale (falling back to <html lang>) and pick a bundled dictionary.

JavaScript API Developers #

window.WPIE.api is the registration surface. api.version is the API semver; feature-detect newer points with if ( api.registerAiTool ) { … }. All ids must be namespaced like Gutenberg blocks: my-plugin/my-thing. Every callback receives { editor, extras } (see The bridge). One example per point:

registerGenerator( generator )

An entry in the Extensions menu that inserts layers. If the layer carries generator = { id, params }, it gets an Edit re-entry in the context menus.

api.registerGenerator( {
  id: 'my-plugin/confetti',
  label: 'Confetti Generator',
  run( { editor, extras } ) {
    const { makeShape } = window.WPIE.bridge.documents;
    const layer = makeShape( { name: 'Confetti', x: 40, y: 40, w: 300, h: 300, pathD: '…' } );
    layer.generator = { id: 'my-plugin/confetti', params: { density: 5 } };
    editor.dispatch( { type: 'ADD_LAYER', layer } );
    editor.commit( 'Insert Confetti' );
  },
  edit( { editor, extras, layer } ) { /* reopen your UI for `layer` */ },
} );

To insert several layers as one unit, an image with its own editable caption for instance, build a makeGroup, dispatch it, then dispatch each child with child.parent = group.id, and select the group with editor.dispatch( { type: 'SET_ACTIVE', id: group.id } ) before you commit. Load any text layers' faces first with bridge.fonts.ensureFontsForLayers( children ), or they paint in a fallback font until the real one arrives.

Dynamic re-render: the resolve hook API 2.11

An optional async resolve makes generator layers dynamic: inside dynamic templates, automations and Preview with Post, the editor asks every generator layer to re-render for the current post context. The hook receives { layer, params, ctx, children, expandTokens, hasTokens, renderTemplate } and may return three shapes: { src, naturalW, naturalH } (fresh pixels for this layer, the 3D Mockup pattern), { hide: true } (the layer disappears for this context, the sale-badge pattern), or, new in API 2.11 for group layers, { group, layers }: a full re-composition where the group and all its children are replaced and child counts may change. That is the repeater case; Showcase Studio re-runs its saved query and rebuilds its cards this way.

Group generators with a resolve hook automatically get a Refresh entry next to Edit in the canvas and layers-panel context menus: one click re-runs the query and swaps the children in place. Convention: return null when the query comes back empty, because stale content beats a blank graphic in a batch run. A throwing resolve never breaks a batch; the design-time layers stay.

registerEffect( effect )

A pure pixel kernel (no DOM, no async). It appears in the Filter menu, the Adjust panel and as a Smart Filter; preview and export share the code path.

api.registerEffect( {
  id: 'my-plugin/scanlines',
  label: 'Scanlines',
  params: {
    gap:    { min: 2, max: 12, default: 3 },       // slider
    strong: { type: 'bool', default: false },      // checkbox
    tint:   { type: 'color', default: '#001122' }, // color
  },
  apply( img, params ) {
    // img = { data: Uint8ClampedArray (RGBA), width, height }. Mutate in place.
    return img;
  },
} );

registerFilterPreset( preset )

A one-click CSS filter preset, like the built-in filter list. Use widely-supported filter functions; the editor emulates them pixel-exactly where ctx.filter is missing.

api.registerFilterPreset( {
  id: 'my-plugin/hero',
  label: 'Hero',
  css: 'saturate(1.4) contrast(1.15)',
} );

registerMenuItem( menuId, item )

menuId is one of file, edit, image, layer, select, filter, view, tools, extensions, help (plus the automation submenus). Items appear behind a divider at the end of the menu.

api.registerMenuItem( 'filter', {
  label: 'My Action…',
  run( { editor, extras } ) {
    extras.toasts.success( 'Layers: ' + editor.state.layers.length );
  },
  when: ( { editor } ) => !! editor.state.activeId, // optional enable-gate
} );

registerPanel( panel )

A tab in the right dock. render gets a plain DOM element (mount any framework yourself) and re-runs when layers or the active layer change; return a cleanup function.

api.registerPanel( {
  id: 'my-plugin/inspector',
  title: 'Inspector',
  render( el, { editor, extras } ) {
    el.innerHTML = '<div style="padding:12px">…</div>';
    return () => {/* cleanup */};
  },
} );

registerPanelSection( section )

A section in the Properties panel, gated per layer via when. Read live values from editor.state inside handlers, not from captured variables.

api.registerPanelSection( {
  id: 'my-plugin/confetti-props',
  title: 'Confetti',
  when: ( layer ) => layer.generator?.id === 'my-plugin/confetti',
  render( el, { editor, extras, layer } ) {
    el.innerHTML = '<label>…controls…</label>';
    return () => {/* cleanup */};
  },
} );

registerTool( tool )

A button in the left tool rail whose pointer handlers dispatch like built-in tools. Handlers receive the tool context tc, the DOM event and the point in document coordinates.

api.registerTool( {
  id: 'my-plugin/stamp-circle',
  label: 'Circle Stamp',
  icon: '<svg viewBox="0 0 24 24" width="18">…</svg>',
  handlers: {
    onDown( tc, event, p ) { /* start */ },
    onMove( tc, event, p ) { /* drag */ },
    onUp( tc, event, p ) { tc.editor.commit( 'Circle Stamp' ); },
  },
} );

registerLibrarySection( section )

A chip in the Asset Library tray, the home for asset packs. items is a static array or a function of the query (may return a Promise, so remote sources work).

api.registerLibrarySection( {
  id: 'my-plugin/doodles',
  label: 'Doodles',
  items: [
    {
      name: 'Heart',
      preview: 'data:image/svg+xml;utf8,…', // any image URL
      asset: { kind: 'user-element', descriptor: { name: 'Heart', pathD: 'M …' } },
    },
  ],
} );

registerTemplatePack( pack )

Own gallery categories (namespaced since API 2.10): declare categories: [ { id, label } ] and reference them via descriptor.category. Reusing a built-in id (sale, event, food, …) files your templates into that chip on purpose; any other id is automatically prefixed with your vendor slug, so two packs declaring yoga get two chips and can never collide. Give every descriptor a stable id (fallback is a slug of the name); duplicate pack.id registrations throw.

Installable starter templates that join the built-in ones. templates is an array or a function returning one (a Promise works, so large packs fetch a JSON asset lazily).

// The script's own URL makes fetches package-relative.
const baseUrl = ( document.currentScript?.src || '' ).replace( /[^/]*$/, '' );

api.registerTemplatePack( {
  id: 'my-plugin/holiday',
  label: 'Holiday Templates',
  templates: () =>
    window.fetch( baseUrl + 'templates.json' ).then( ( r ) => r.json() ),
} );

registerAiTool( tool )

A button in AI Studio, Local tools. It runs inside the panel busy/error harness; use the bridge for self-hosted transformer models, so inference never leaves the browser.

api.registerAiTool( {
  id: 'my-plugin/name-layers',
  label: 'Name Layers',
  title: 'Names every image layer with a local vision transformer.',
  async run( { editor, extras, layer } ) {
    const TF = await window.WPIE.bridge.ml.loadTransformers();
    const pipe = await TF.pipeline( 'image-to-text', 'Xenova/vit-gpt2-image-captioning' );
    // … caption layers, dispatch UPDATE_LAYER patches, commit.
  },
} );

registerExportFormat( format )

Adds a button to the Export dialog. render() gives you the flattened canvas from the real pipeline; encode must return a Blob.

api.registerExportFormat( {
  id: 'my-plugin/tiff',
  label: 'TIFF',
  ext: 'tiff',
  async encode( { doc, layers, render, scale, quality } ) {
    const canvas = await render();
    return myTiffEncoder( canvas );
  },
} );

Effects are stored by id in the project JSON; if a project is opened without your extension the effect is skipped gracefully and preserved in the file, so no data is lost.

registerBinding( binding ) API 2.10

A custom dynamic-content variable: {{my-plugin/name}} tokens then resolve everywhere core tokens do (bound text layers, QR contents, chart titles, batch runs, Preview with Post) and the variable appears in every {…} picker under your group label. resolve( ctx ) is synchronous and receives the run’s post context; throwing or returning nothing degrades to an empty string, so a broken resolver never kills a batch.

api.registerBinding( {
  id: 'my-plugin/reading-time',
  label: 'Reading time',
  group: 'My Plugin',
  resolve: ( ctx ) => {
    const words = String( ctx.fields?.[ 'post.content' ] || '' ).split( /\s+/ ).length;
    return Math.max( 1, Math.round( words / 220 ) ) + ' min';
  },
} );

PHP hooks Developers #

For WordPress-plugin extensions that need server-side behavior:

HookTypePurpose
wpie_editor_assetsaction ( $attachment_id )Enqueue editor-page scripts and styles.
wpie_bootstrap_datafilter ( $data, $attachment_id, $is_new )Add values to window.WPIE.
wpie_stock_providersfilter ( $providers )Register stock-image sources.
wpie_after_saveaction ( $attachment_id, $file, $context )React to saves (save = overwrite, saveAs = new attachment).
wpie_settings_tabsfilter ( $tabs )Add a tab to the settings page.
wpie_settings_extra_tabsaction ( $active_tab )Render your added settings tab.
wpie_ai_modelsfilter ( $models, $provider )Adjust the AI model list per provider.

The bridge Developers #

window.WPIE.bridge hands extensions the editor’s own building blocks, so they never bundle their own copies. It is additive: members are never renamed or removed. The full surface:

Documents, rendering & assets

  • documents: layer factories and (de)serialization, makeShape, makeText, makeImage, makeGroup, createBlankDoc, loadImage, hydrateLayers, serializeLayers.
  • raster: renderToCanvas, renderToBlob, sharedImageCache, and docBackdrop( editor.state, { exclude } ) (API 2.10) – the document rendered without one group, the ready-made “Show document” preview backdrop.
  • svg: exportSvg (keeps dynamic-binding markers), importSvg.
  • fonts: ensureFontsForLayers. icons: the Tabler icon set. aspect: nearestAspect.

Data, templates & export

  • dynamicContent: resolveBindings, prepareGeneratorLayers (re-render generator layers per post), collectMetaKeys, plus the token helpers expandTokens, hasTokens and bindingGroups for {{token}} fields and pickers.
  • templatesLib: loadTemplate, invalidateTemplate, renderResolved (a template plus post context to a finished canvas).
  • csv: parseCsv. exportPresets: listExportPresets. watermark: applyWatermark. providers: PROVIDER_LABELS, hasTextProvider.
  • macros: listMacros, runMacro, createHeadlessEditor (run edits off-screen).

AI & local models

  • ml: loadTransformers (the self-hosted transformers.js runtime), mlPipeline with WebGPU auto-fallback, preferredDevice, isModelInstalled, modelsBaseUrl. Inference never leaves the browser.
  • api.ai.complete( { prompt, system, schema, tier, maxTokens } ) (API 2.10): one generic text call against the site’s configured providers – plain text, or schema-shaped JSON when you pass a JSON Schema ({ data }). Tier caption is fast and cheap, design is strong; calls are rate-limited, metered and budget-capped like every other AI action. Feature-detect with bridge.providers.hasTextProvider().

The 2.10 toolbox API 2.10

  • ui: framework-free DOM builders that emit the editor’s shared design-system classes – dialog (the studio dialog shell), section (icon-headed settings card), row, slider, select, check, btn, el. A studio dialog matches the editor CI without local helpers or CSS.
  • storage: get( ns ) / set( ns, object ) – one JSON object (up to 32 KB) per extension namespace per user, stored server-side. The right home for favorites and toggles that localStorage would lose on the next device.
  • brand: kits(), kitById( id ) and activeKitColors( doc.brandKitId ) – the palette resolution (document’s active kit, then site kit, then first usable kit).
  • video: recordCanvas( canvas, { fps, durationMs, bitsPerSecond, onProgress } ){ stop, blob, mimeType } with the vp9 → vp8 → webm fallback chain, for WebM exports.
  • util: rng( seed ) (deterministic mulberry32, safe for stored designs), EASINGS, downloadBlob.
  • colors: the Color Schemer’s engine (buildScheme, randomSchemeInput, schemeRoles, labelColorFor), extractPalette( image, k ) (dominant colors) and contrastRatio (WCAG).
  • qr: the editor’s QR engine – payload, ready, render( payload, style ){ canvas, modules, ecl } with styled modules and eyes (draw result.canvas), warnings.
  • optimize (core 1.297): optimizeCanvas( canvas, { format: 'auto'|'webp'|'jpeg', target: 'auto'|1..100, maxDim, ssimTarget, encode } ) re-encodes a canvas as the smallest file with no visible difference (perceptual SSIM search, WebP-preferring; encode since 1.298 accepts a custom encoder with the encodeCanvas contract), plus encodeCanvas, fitCanvas, ssimLuma.

Editor UI components

  • components: mount the editor’s own controls into your extension, framework-free. mountColorButton( node, { color, onChange, title } ) renders the native color swatch and picker (returns { set, unmount }); mountVarButton inserts {{token}} variables; mountStyleButton is the AI style-preset picker; HelpLink is the standard help link.
  • mountFontPicker( node, { value, onChange, width, families } ) (API 2.9, families since 2.10): the editor’s font selector – a grouped dropdown (Brand / Custom / System fonts / Editor fonts / Google Fonts) that previews every family in its own face and lazy-loads the preview faces. value is a family name, onChange( family ) fires on pick; pass families (array) to restrict the catalog to a curated allow-list. Returns { set, unmount }. Feature-detect it and keep a plain <select> as the fallback on older cores; call bridge.fonts.ensureFont( family ) before painting the family to a canvas.
  • mountKitPicker( node, { value, onChange, allowEmpty } ) (API 2.10): the brand-kit dropdown. Controlled and self-updating; renders nothing when the site has no kits. Resolve the actual palette with bridge.brand.activeKitColors( id ).
  • mountMediaPicker( node, { onPick, height, selectedIds } ) (API 2.10): the editor’s in-modal media chooser with title and local semantic search and endless scroll. onPick receives { id, url, fullUrl, title }; the host owns selection (pass selectedIds for multi-select display, update via set( ids )).
  • mountIconPicker( node, { onPick, icons, emoji, height } ) (API 2.10): search plus tabs over the bundled Tabler icons and emoji as an inline panel; onPick receives { type: 'icon', name, path } or { type: 'emoji', char, name }. It positions nothing itself: to open it as a popover, append it to the dialog backdrop (not the dialog box, which is position:static) and place it from the button viewport rect. See the example below.

Open the icon/emoji picker as a popover at a click. The dialog box is position:static, so anchor the popover to the backdrop (fixed, viewport-filling) and use the button viewport rect (an icon keeps its name and path, an emoji its char):

// modal = bridge.ui.dialog() -> { backdrop, dialog, ... }
const pop = document.createElement( 'div' );
pop.style.cssText = 'position:absolute;z-index:50';
modal.backdrop.appendChild( pop );          // NOT modal.dialog (static)
const r = anchor.getBoundingClientRect();
pop.style.left = Math.max( 8, Math.min( r.left, innerWidth - 308 ) ) + 'px';
pop.style.top  = ( r.bottom + 344 > innerHeight ? r.top - 350 : r.bottom + 6 ) + 'px';
const h = bridge.components.mountIconPicker( pop, {
  icons: true, emoji: true,
  onPick: ( sel ) => {
    sel.type === 'icon' ? useIcon( sel.name, sel.path ) : useEmoji( sel.char );
    h.unmount(); pop.remove();
  },
} );
// draw an icon: g.stroke( new Path2D( sel.path ) ), scaled by size / 24

Server-backed helpers (bridge.api)

  • posts (post and product catalog / search), templates, automation, and ai (image and text generation).
  • geo: a capability-gated, server-side OpenStreetMap proxy for map extensions (geo.search, geo.map), cached on your server.
  • Media helpers: saveToLibrary, saveAsNew, buildFormData, getMediaMeta, updateMediaAlt, proxyImageUrl.
The callback context

Every registration callback receives { editor, extras }. editor is the live editor: state (doc, layers, selection, activeId), dispatch( action ), commit( label ) (a history snapshot), undo() and redo(). extras carries toasts and around forty dialog openers, so an extension can launch core features directly, for example extras.openColorSchemer(), extras.openChart(), extras.openQr(), extras.openStock(), extras.openGenerate(), extras.openMockup(), extras.openExport() and many more.

REST: 3D model library & Meshy proxy Developers #

Since core 1.295 two server-side services back the 3D studios and are open to every extension via window.wp.apiFetch (nonce handling included). Namespace wpie/v1, editor capability required.

Model library

GLB storage under uploads/wpie-3d-models/, shared per site: reads for every editor user, rename and delete for the owner or an administrator. Uploads are validated by the GLB magic bytes and capped at 50 MB (filter wpie_3d_model_max_bytes), 100 models per site; models and their PNG thumbnails travel with plugin backups.

  • GET /wpie/v1/models3d[ { id, name, size, created, source, mine, url, thumb } ]
  • POST /wpie/v1/models3d – multipart, field file (.glb) plus optional name
  • POST /wpie/v1/models3d/<id>{ name? } renames (owner/admin); { thumb? } stores a data-URL PNG preview (any editor user, ≤ 300 KB)
  • DELETE /wpie/v1/models3d/<id> – owner/admin

Meshy proxy

AI 3D generation with the site's stored key – it never reaches the browser. The site owner connects Meshy under Settings → Integrations; without a key every route returns wpie_meshy_key. The site-wide model and detail-quality defaults are applied server-side.

  • POST /wpie/v1/meshy/generate{ mode: 'image', attachment }, { mode: 'text', prompt } or { mode: 'text-refine', preview_id }{ id, mode }
  • GET /wpie/v1/meshy/task?mode=&id={ status, progress, thumbnail, error } – poll every ~5 s
  • POST /wpie/v1/meshy/import{ mode, id, name? } downloads the finished GLB into the model library and keeps Meshy's preview as the thumbnail

Rules of the road Developers #

  • Namespace everything (my-plugin/thing); unprefixed ids are rejected.
  • Effects must be pure pixel functions: no DOM, no async, no state. They run in the live preview on every frame; keep them linear over pixels.
  • Detect API capabilities with api.version or !! api.registerGenerator instead of failing hard on older editor versions.
  • Projects that reference a missing extension effect still open; the effect is skipped at render and preserved in the file.
  • The editor is GPL; extensions interacting this deeply are derivative works, license accordingly.