{"$schema":"http://json-schema.org/draft-07/schema#","$id":"https://api.myorbitcommerce.net/v1/plugin-schema.json","title":"Orbit Commerce plugin manifest (normalised)","description":"The Orbit Commerce plugin file format, served at GET /v1/plugin-schema.json. It began as a copy of the @orbitcommerce/sdk 3.1 plugin.schema.json and now adds $schema and listing, in 3.2 the extension point icon and handle, and in 3.3 lifecycle.connectUrl; until the SDK copy is synced, this file is the source of truth. The API validates releases against this plus the extension-target registry, the scope catalog and the URL/environment rules.","type":"object","additionalProperties":false,"required":["version","extensionPoints","oauth"],"properties":{"$schema":{"type":"string","description":"Optional. Point it at this schema's URL so editors can check and complete the file. Ignored by the API."},"pluginId":{"type":"string","minLength":3,"maxLength":100,"pattern":"^[a-z0-9][a-z0-9-]*[a-z0-9]$"},"version":{"type":"string","pattern":"^\\d+\\.\\d+\\.\\d+$"},"baseUrl":{"type":"string","minLength":1,"format":"uri"},"additionalOrigins":{"type":"array","items":{"type":"string"}},"extensionPoints":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["target","url"],"properties":{"target":{"type":"string","pattern":"^[a-z]+(\\.[a-z]+(-[a-z]+)*){2}$"},"url":{"$ref":"#/definitions/pathOrAbsoluteUrl"},"label":{"type":"string","maxLength":100},"optionsUrl":{"$ref":"#/definitions/pathOrAbsoluteUrl"},"optionsParam":{"type":"string","minLength":1,"maxLength":100},"optionsLabel":{"type":"string","maxLength":100},"icon":{"type":"string","minLength":1,"maxLength":40,"pattern":"^[A-Z][A-Za-z0-9]*$","description":"A lucide component name (Gift, ShoppingBag) shown beside the label where the host lists the extension point."},"handle":{"type":"string","pattern":"^[a-z][a-z0-9-]{1,39}$","description":"The path segment the host serves the extension point under. Required for storefront.account.section (/account/<handle>); that rule, the reserved core handles and uniqueness are the semantic validator's."}}}},"oauth":{"type":"object","additionalProperties":false,"required":["scopes"],"properties":{"scopes":{"type":"array","uniqueItems":true,"items":{"type":"string","minLength":1}},"redirectUrls":{"type":"array","items":{"$ref":"#/definitions/pathOrAbsoluteUrl"}},"webhooks":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["topic","path"],"properties":{"topic":{"type":"string","minLength":1,"pattern":"^[a-z][a-z0-9_-]*(\\.[a-z][a-z0-9_-]*)+$","description":"Dot-separated segments, each lowercase alphanumeric/underscore/hyphen (e.g. product.created, product.variants.synced, custom_field_value.updated). Structural gate only: whether the topic is a real, subscribable topic is decided by the semantic validator's TOPIC_UNKNOWN check against SUPPORTED_WEBHOOK_TOPICS, not here."},"path":{"$ref":"#/definitions/pathOrAbsoluteUrl"}}}}}},"ui":{"type":"object","additionalProperties":false,"properties":{"menu":{"type":"object","additionalProperties":false,"required":["label","icon"],"properties":{"label":{"type":"string","minLength":1},"icon":{"type":"string","minLength":1},"position":{"type":"integer","minimum":0},"parent":{"type":"string","enum":["products","orders","customers","marketing","settings"]}}},"dashboard":{"type":"object","additionalProperties":false,"properties":{"widgets":{"type":"array","items":{"type":"string"}}}},"customPages":{"type":"object","additionalProperties":false,"properties":{"main":{"$ref":"#/definitions/relativePath"},"settings":{"$ref":"#/definitions/relativePath"},"designer":{"$ref":"#/definitions/relativePath"}}}}},"settingsSchema":{"type":"object"},"lifecycle":{"type":"object","additionalProperties":false,"properties":{"connectUrl":{"$ref":"#/definitions/pathOrAbsoluteUrl","description":"Where the platform POSTs plugin.connect when a store's install becomes active, with a session token for the install as the bearer, so the plugin can exchange it for its access and refresh pair without a staff member opening its page. A path is joined onto baseUrl; an absolute URL must sit on baseUrl's origin or one of additionalOrigins."}}},"staffPermissions":{"type":"array","maxItems":50,"description":"Permissions a store hands its own staff for this plugin's screens. The role editor lists them under the plugin's name; the session token carries the keys the user holds.","items":{"type":"object","additionalProperties":false,"required":["key","label"],"properties":{"key":{"type":"string","maxLength":64,"pattern":"^[a-z][a-z0-9-]*(:[a-z][a-z0-9-]*){1,2}$"},"label":{"type":"string","minLength":1,"maxLength":80},"default":{"type":"string","enum":["owner","admin","staff"],"description":"The lowest store system role that holds it without a grant. Defaults to admin."}}}},"branding":{"type":["object","null"],"additionalProperties":false,"properties":{"tileBackground":{"type":["string","null"]},"iconFit":{"type":"string","enum":["cover","contain"]},"iconScale":{"type":"number","minimum":0},"monochromeIconPath":{"type":["string","null"]}}},"listing":{"type":"object","additionalProperties":false,"description":"Plugin store copy. Not configuration: releases ignore it. Creating a plugin or a draft from the file uses these values when the matching top-level field is absent.","properties":{"name":{"type":"string","minLength":2,"maxLength":100},"shortDescription":{"type":"string","maxLength":160},"description":{"type":"string","maxLength":5000},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"features":{"type":"array","items":{"type":"string"}},"supportUrl":{"type":"string","format":"uri"},"documentationUrl":{"type":"string","format":"uri"},"pricing":{"type":"object","additionalProperties":false,"required":["model"],"properties":{"model":{"type":"string","enum":["free","one_time","subscription"]},"price":{"type":"number","minimum":0},"currency":{"type":"string"},"interval":{"type":"string","enum":["monthly","yearly"]},"trialDays":{"type":"integer","minimum":0},"freePlanAvailable":{"type":"boolean"}}}}}},"definitions":{"relativePath":{"type":"string","pattern":"^/(?!/)","description":"A path relative to the host, starting with exactly one slash. Protocol-relative (//host) is rejected: it would silently point at another origin."},"pathOrAbsoluteUrl":{"type":"string","minLength":1,"pattern":"^(?:/(?!/)|https?://)","description":"Either a relativePath or an absolute http(s) URL. Absolute is legal in the normalised shape (a plugin may serve a widget from an additionalOrigins host); whether the origin is allowed is decided by the URL/environment rules, not here. Protocol-relative is rejected, and so is every non-http(s) scheme (javascript:, data:, file:, vbscript:, ...): nothing here is ever rendered as a scheme other than an in-app path or an https widget/redirect/webhook target."}}}