The salieno.json manifest
Every field in a payment gateway's salieno.json manifest, key by key — the schema Core validates, the capabilities that gate the UI, and the credentials that render the Auto Gateways config form.
On this page
Every payment gateway extension ships two things and no more: a driver class and a salieno.json manifest that sits next to it. A gateway is code only — there are no views to package. The manifest is how Core learns your gateway exists before it ever loads a line of your code: it names the gateway, points at the driver class, states which Core versions it targets, and declares the two things Core acts on generically — the capabilities that gate the UI and the credentials that render the Auto Gateways config form. This article documents every field.
For the driver methods it points at, see The driver contract. For what each capability lights up on screen, see Capabilities and the UI.
A complete manifest#
Here is the real Stripe manifest, annotated. The comments are for reading only — the file on disk must be strict, comment-free JSON.
{
"schema": "salieno.payment/1", // manifest format version — always this string
"kind": "payment", // this is a payment gateway (not a registrar or panel)
"slug": "stripe", // unique id: the gateway's join key AND the marketplace product slug
"name": "Stripe", // shown in the Extensions library + Auto Gateways
"version": "1.0.0", // YOUR gateway's semver
"namespace": "Salieno\\Payment\\Stripe", // your PSR-4 root (trailing backslash optional)
"driver": "Gateway", // the class in that namespace implementing the contract
"entry": "Gateway.php", // the driver file, relative — must end in .php
"requires_core": ">=1.0.0 <2.0.0", // the CORE version window you support (not your version)
"author": "Salieno", // optional; marketplace listing metadata
"description": "Accept card payments worldwide through Stripe Checkout...", // optional blurb
"homepage": "https://stripe.com/docs", // optional; vendor API docs
"capabilities": [ // gates the UI cheaply; driver capabilities() is authoritative
"charge", "refund", "partial_refund", "webhook", "redirect", "3ds"
],
"credentials": [ // renders the Auto Gateways config form, field by field
{ "key": "secret_key", "label": "Secret Key", "type": "password", "required": true, "global": true,
"help": "Your Stripe secret key (sk_live_… / sk_test_…). Dashboard > Developers > API keys." },
{ "key": "publishable_key", "label": "Publishable Key", "type": "text", "required": false, "global": true,
"help": "Your Stripe publishable key. Optional for the hosted Checkout flow." },
{ "key": "webhook_secret", "label": "Webhook Signing Secret", "type": "password", "required": true, "global": true,
"help": "The signing secret (whsec_…) for a webhook pointed at /client/ipn/stripe. Payments will NOT settle without it." }
]
}Top-level fields#
schema#
Always "salieno.payment/1". Core compares this exactly; anything else is rejected at install with "that gateway uses a manifest format this Salieno version does not understand." It is how a future manifest revision can change shape without breaking installs of the current one.
kind#
Always "payment". It distinguishes a payment-gateway extension from the other marketplace extension kinds (registrars, panels, themes). A wrong value is refused as "that extension is not a payment gateway."
slug#
The gateway's unique identifier, lowercase, [a-z0-9] start then [a-z0-9_-]. This one string does triple duty, and it is the sharpest edge in the whole manifest:
- It is the marketplace product slug — the id of the listing a buyer is entitled to.
- It is the immutable join key. Every config row, every
gateway_currenciesrow, and all payment history are keyed to this slug. It is what an installed gateway is called forever after. - It is the path segment in your webhook URL — the buyer's browser return and the server-to-server webhook both arrive at
/client/ipn/<slug>(Stripe's is/client/ipn/stripe). That URL is what the operator registers at Stripe or PayPal.
On install, Core checks that the manifest slug matches the product it was fetched as (compared case-folded); a mismatch fails with "that gateway package does not match the product it was fetched as." Because config, currencies and history all reference it — and because operators pin it into a webhook endpoint at the gateway vendor — an update must never change it. Changing the slug orphans every saved credential and breaks the live webhook. Pick it once, at creation, and leave it.
name#
The human label shown in the admin Extensions library and the Auto Gateways list. Free text — "Stripe", "PayPal".
version#
Your gateway's own semantic version, e.g. "1.0.0". This is the number the marketplace tracks for update badges: to publish an update you increment it, and its semver must be strictly greater than the currently published version. It is stored on the installed row and shown next to your gateway in the library. Do not confuse it with requires_core below.
namespace#
Your PSR-4 root, written as a PHP namespace — "Salieno\\Payment\\Stripe". In JSON each backslash is escaped, so a single \ is written \\. The trailing backslash is optional (Core normalises either way). Core maps this namespace onto your package files through a scoped autoloader that is unlocked only after the security gate passes, so your namespace should be distinctive to your gateway. It is validated for a conservative namespace shape at install, failing with "that gateway declares an invalid namespace."
driver#
The class within that namespace that implements the contract — "Gateway". Core resolves the fully qualified class as namespace + driver, so here Salieno\Payment\Stripe\Gateway. If your driver lives in a sub-namespace you may write "Drivers\\Gateway", and Core will look for the class at Salieno\Payment\Stripe\Drivers\Gateway. The resolved class must be instanceof App\PaymentGateways\GatewayInterface; extending App\PaymentGateways\AbstractGateway satisfies that automatically (see The driver contract). An invalid value is refused as "that gateway declares an invalid driver class."
entry#
The driver file's path relative to the package root — "Gateway.php". It must end in `.php`; the marketplace requires it when you submit. It should point at the same file namespace + driver resolves to: strip the namespace root off the fully qualified class, turn \ into /, add .php. So the Stripe class resolves to Gateway.php at the package root, and a "Drivers\\Gateway" driver resolves to Drivers/Gateway.php. Note that the Core installer does not read entry to find the driver — it derives that path from namespace + driver and checks it exists, failing with "that gateway does not contain the driver it declares." Point entry at that same file so the marketplace and Core agree.
requires_core#
A Core version range, e.g. ">=1.0.0 <2.0.0". This is the single most-misread field: it declares which versions of Salieno Core your gateway is compatible with, not anything about your gateway's own version. It must be one continuous range — no prerelease or hyphen ranges. Use a range that excludes the next major (<2.0.0) so a breaking Core release does not silently run an untested gateway against it.
author, description, homepage#
All optional, all purely informational for the marketplace listing — author is a display credit, description a one-line blurb, homepage a URL (Stripe points it at the vendor's API docs). Core stores them but does not act on them.
The two validators#
A gateway package is checked twice, and the manifest has to satisfy both.
- The marketplace, when you submit, needs
kind,name,version,requires_coreand anentrypointer. - The Core installer (
MANIFEST_SCHEMA = 'salieno.payment/1'), when an operator installs from the Extensions library, needsschema,kind,slug,namespaceanddriver, and then confirms the driver file thatnamespace+driverresolves to actually exists in the package.
The two required sets overlap only on kind. Include every field above and both validators pass; drop one that only the other cares about and the package is rejected at the stage that needs it. The annotated manifest is the union of both — copy its shape.
The capabilities array#
capabilities is the manifest's first big payoff. It is a flat array of capability keys, and Core reads it to gate the UI without loading your driver — the array rides inside the signed, hash-checked manifest, so Core can decide which controls to render (checkout, the admin "refund to gateway" button, eligibility for recurring renewals) without paying to load and run a line of your code.
It is not authoritative at runtime. The moment an operation actually runs, Core trusts your driver's capabilities() method, never this array. Keep the two identical — the manifest gates cheaply, the driver decides for real. Stripe's manifest array and its driver method match exactly:
public function capabilities(): array
{
return [
PaymentCapability::CHARGE,
PaymentCapability::REFUND,
PaymentCapability::PARTIAL_REFUND,
PaymentCapability::WEBHOOK,
PaymentCapability::REDIRECT,
PaymentCapability::THREE_D_SECURE, // "3ds"
];
}The valid keys are: charge, refund, partial_refund, webhook, redirect, hosted_fields, recurring, crypto, 3ds (PaymentCapability::ALL). What each one lights up is documented in Capabilities and the UI. Two of them gate real behaviour rather than mere chrome: `refund` enables the admin "refund to gateway" path — omit it and the admin issues a wallet credit instead — and `recurring` is what makes a gateway offerable for automatic renewals. `charge` is mandatory: every automatic gateway must declare it (and capabilities() must override the abstract default to include at least PaymentCapability::CHARGE).
PayPal is the instructive contrast — it declares only four (charge, refund, partial_refund, redirect), omitting webhook (it settles by capturing on the buyer's return, not over a server-to-server webhook) and 3ds. Declare exactly what your driver implements. A capability listed here but missing from the method paints a control that then fails; the reverse hides a path your driver really backs.
The wallet-credit refund fallback is not a capability — it is Core's own behaviour when a gateway does not declare refund, so there is nothing to add to the array to get it.
The credentials list#
credentials is why adding a gateway needs no changes to Core. It is a list (not an object) of field definitions; Core renders the Auto Gateways config form straight from it, one input per entry, in the order given. Each entry:
| Key | What it does |
|---|---|
key | The field's storage key — what your driver reads back with $this->getParam($ctx, 'secret_key'). |
label | The form label shown above the input ("Secret Key", "Webhook Signing Secret"). |
type | "text", "password" or "select". password masks the input; select renders a dropdown (needs options). |
required | Boolean. Core blocks saving the config until every required field is filled. |
global | Boolean. Whether this value is shared across all currencies or entered per-currency (see below). |
options | `select` only — a { "value": "Label" } map of the dropdown choices. |
help | Inline help under the field — where to find the value, gotchas, what breaks without it. |
The three field types cover every gateway. Stripe uses password for its two secrets and text for the optional publishable key. PayPal uses a select for its live/sandbox switch — the canonical select example, options and all:
{ "key": "mode", "label": "Mode", "type": "select", "required": true, "global": true,
"options": { "live": "Live", "sandbox": "Sandbox" },
"help": "Use Sandbox with sandbox credentials to test, Live to take real payments." }The driver reads that stored value back exactly like any other field — $this->getParam($ctx, 'mode'). (Core also exposes a per-currency sandbox toggle through $this->isSandbox($ctx) for gateways that distinguish environments by key prefix, like Stripe's sk_test_ vs sk_live_, and so never need a visible mode field. Declare a select when your API needs an explicit endpoint switch; lean on isSandbox() when it doesn't.)
global: per-currency vs fanned-out#
global is the payment-specific field. Credentials are stored per-currency, in the encrypted gateway_currencies.gateway_parameter (the column is $hidden and encrypted:object — never hardcode a secret), and read back per call:
protected function secretKey(Deposit|GatewayCurrency $ctx): ?string
{
return $this->getParam($ctx, 'secret_key') ?: null; // $ctx is the Deposit during checkout
}That works at checkout because Core hands process() the Deposit, which knows its currency row. But a webhook has no Deposit and no currency in hand — it arrives cold at /client/ipn/<slug>. A field marked "global": true is fanned across every currency row at save time, so the same value exists on all of them and can be resolved without a currency, through ownConfig():
protected function webhookSecret(): ?string
{
$config = $this->ownConfig(); // your OWN global config — no slug hardcoded
return $this->getParam($config, 'webhook_secret') ?: null;
}Mark anything a webhook must read — signing secrets, single-account API keys — as "global": true. Leave a field non-global only when it genuinely differs per currency (a per-currency merchant account). Stripe and PayPal mark all of their credentials global, because one API key serves every currency. And note webhook_secret is "required": true on purpose: an empty signing secret makes an attacker-forged event pass an empty-key HMAC, so the driver must reject a blank one — required keeps the operator from ever saving it empty. The full config-form and testConnection walkthrough is in Credentials and the config form.
Packaging#
Zip the manifest and the driver at the root of the archive:
stripe.zip
salieno.json
Gateway.phpOne extra level of nesting is tolerated — Core records the wrapper as a root_prefix and strips it on extract — but a flat root is cleanest. Any helper classes go under your namespace next to the driver. You do not sign the package locally: on marketplace approval the signature is added server-side, and Core verifies it against a pinned key at install and again every time the driver loads. The full submit-and-publish flow is in Publishing to the marketplace.
Sharp edges, in one place#
- `slug` is the marketplace product slug, the immutable join key for all config/currencies/history, and the path segment in your `/client/ipn/<slug>` webhook URL — all at once. Choose it at creation and never change it; an update that changes it orphans every saved credential and breaks the endpoint the operator registered at the gateway vendor.
- `entry` must end in `.php` and should name the same file
namespace+driverresolves to; install fails if that namespace-derived file is not in the package. - `requires_core` is the Core version window, not your gateway's version. Your version lives in
version, as a single continuous range with no prerelease. - The manifest satisfies two validators — the marketplace (
kind,name,version,requires_core,entry) and the Core installer (schema,kind,slug,namespace,driver, plus the driver file existing). Include all of them. - The `capabilities` array gates the UI cheaply; the driver's `capabilities()` method is the runtime authority. Keep them identical, and always include
charge. - `credentials` is a list.
typeistext,passwordorselect(aselectcarries anoptionsmap). Mark any field a webhook must read"global": trueso it fans across every currency and resolves viaownConfig(); leave truly per-currency fields non-global.
With the manifest in hand, move on to The driver contract to implement the methods it points at.