Submitting a Plugin
This is the operational walkthrough for shipping a plugin to the Levy Fleets marketplace. Read the developer portal overview first for context on the platform and revenue model.
Step 1 — Register as a vendor
Go to /developers and click Register.
You'll provide:
- Vendor name — appears on plugin cards
- Contact email — for review notifications, not displayed publicly
- Support email — displayed publicly, where operators send issues
- Homepage URL
- Logo — 256×256 PNG, square, transparent background
We'll send a verification email. After verification, you sign the Plugin Partner Agreement (standard terms, takes ~5 minutes to read).
Step 2 — Set up Stripe Connect (paid plugins only)
If your plugin will charge anything (one-time, monthly, or usage), you onboard with Stripe Connect Express. From the vendor dashboard:
- Click Set up payouts
- Stripe opens its onboarding flow
- Provide business info, bank account, and tax info (~10 minutes)
- Stripe verifies — typically same-day for US, 1-3 days internationally
Free plugins can skip this step.
Step 3 — Build your webhook endpoint
Your plugin needs an HTTPS endpoint that accepts signed POSTs from Levy. Minimum implementation:
import crypto from 'crypto';
const PLUGIN_SECRET = process.env.LEVY_PLUGIN_SECRET!; // from your vendor dashboard
export async function POST(request: Request) {
const sig = request.headers.get('x-levy-signature');
const body = await request.text();
if (!verifyLevySignature(body, sig, PLUGIN_SECRET)) {
return new Response('Invalid signature', { status: 401 });
}
const event = JSON.parse(body);
// handle event.event_type, event.data
return new Response('ok');
}
function verifyLevySignature(body: string, header: string | null, secret: string) {
if (!header) return false;
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const ts = parts.t;
const v1 = parts.v1;
if (!ts || !v1) return false;
// reject if older than 5 minutes
if (Math.abs(Date.now() / 1000 - parseInt(ts, 10)) > 300) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${ts}.${body}`)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}
Levy retries failed deliveries 5 times with exponential backoff (1m, 5m, 30m, 2h, 12h). Respond with HTTP 2xx within 30 seconds. Anything else is treated as failure.
Step 4 — Author your manifest
Create plugin.json in your project root. Minimal example:
{
"slug": "my-fraud-scorer",
"version": "1.0.0",
"name": "My Fraud Scorer",
"vendor": {
"name": "Acme Fraud Inc",
"email": "support@acmefraud.com",
"url": "https://acmefraud.com"
},
"category": "other",
"description": "Scores every ride for fraud risk and writes a flag back to Levy.",
"long_description_mdx": "# Acme Fraud Scorer\n\nScores rides...",
"screenshots": [
"https://cdn.acmefraud.com/screenshots/dashboard.png"
],
"homepage": "https://acmefraud.com",
"support_url": "https://acmefraud.com/support",
"permissions": ["read:rides", "read:rides.geo", "write:rides.flags"],
"webhook_subscriptions": ["ride.ended"],
"webhook_endpoint": "https://api.acmefraud.com/levy/webhook",
"config_schema": {
"type": "object",
"required": ["api_key"],
"properties": {
"api_key": {
"type": "string",
"title": "Acme Fraud API Key",
"description": "Find this in your Acme Fraud account → Settings → API"
},
"threshold": {
"type": "number",
"title": "Risk threshold (0-100)",
"default": 70
}
}
},
"pricing": {
"model": "usage",
"unit": "ride scored",
"unit_amount_cents": 5,
"currency": "USD"
}
}
config_schema tips
- Use
titleanddescriptionon every field — they're shown to operators in the install form. - Mark required fields with
required: ["field_name"]. - Use
format: "password"for API keys — Levy renders them as masked inputs. - Use enums for picklists:
"enum": ["option_a", "option_b"]. - Use
format: "uri"for URLs to get inline validation.
Permission selection tips
Operators see your permissions in plain English. Each scope you request makes the install dialog longer and more intimidating. The best strategy: ask for the minimum and explain why in your long_description_mdx.
Step 5 — Test in sandbox
From the vendor dashboard, request Sandbox access. You'll be issued a sandbox subaccount with synthetic data.
Test these scenarios:
- Install from sandbox marketplace — your plugin appears in the sandbox marketplace once submitted.
- Fire a test event — use Vendor dashboard → Sandbox → Fire test event to send a synthetic
ride.endedto your endpoint. Verify your signature check passes and your handler does the right thing. - Test the config form — does it render correctly? Are validation errors clear?
- Test OAuth (if applicable) — does the authorize_url redirect correctly? Does the token_url accept the auth code?
- Test webhook failure handling — return a 500 from your endpoint once. Verify Levy retries and eventually marks the install as failing.
Step 6 — Submit for review
From the vendor dashboard, click Submit for review. Upload:
- Your
plugin.json(parsed against schema) - All screenshot URLs (verified accessible)
- Your long description (Markdown, max 20KB)
- Pricing details for confirmation
- A brief note to the reviewer (optional, helpful for context — "this is v2 with the new sandbox flow")
The submission moves to the Levy ops review queue. You'll get email updates as it progresses.
Step 7 — Address review feedback
The reviewer either approves your plugin or requests changes. Common requests:
- Reduce permission scope — "your plugin asks for
read:customers.piibut the manifest description doesn't justify it. Either remove the scope or expand the description." - Improve description — "the description doesn't explain what this plugin does in plain language."
- Add screenshots — "we need at least one screenshot showing the plugin in action."
- Fix signature verification — "we sent a test event with a bad signature and your endpoint returned 200. Please verify signatures correctly."
- Adjust pricing — "$999/mo is the maximum unless you have a custom agreement. Either lower the price or contact us about an exception."
Re-submit by updating your plugin in the dashboard and clicking Re-submit. The 5-day SLA clock restarts; re-submissions usually clear in 1-2 days.
Step 8 — Launch
Once approved, your plugin goes live in the marketplace within minutes. Operators can install immediately. You'll see install counts and active installs on your vendor dashboard.
We recommend a soft launch:
- Quietly verify the first 5 installs work end-to-end
- Watch the webhook success rate
- Fix any production issues before promoting
- Then promote via your channels (LinkedIn, your existing customers, etc.)
Levy may co-market your plugin if it's strategically valuable — reach out to partnerships@levyelectric.com to coordinate.
Versioning after launch
- Patch (1.0.0 → 1.0.1) — bug fixes, no API changes. Auto-upgrades for all installs.
- Minor (1.0.0 → 1.1.0) — new optional features, new optional config fields. Auto-upgrades.
- Major (1.0.0 → 2.0.0) — breaking changes (new required permissions, removed config fields, changed webhook payload format). Operators see a banner asking them to re-consent. Old version continues working until they upgrade.
Use semver responsibly. Major bumps are friction — bundle multiple breaking changes into one release.
What's next
- Developer portal — overview, agreements, sandbox
- Plugin billing — how Stripe Connect payouts work