advanced
plugins
developer-portal
submission

Submitting a Plugin

Step-by-step guide to submitting a plugin to the Levy Fleets marketplace. Vendor registration, manifest authoring, sandbox testing, and submission.

Levy Fleets TeamMay 18, 20268 min read

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:

  1. Click Set up payouts
  2. Stripe opens its onboarding flow
  3. Provide business info, bank account, and tax info (~10 minutes)
  4. 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 title and description on 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:

  1. Install from sandbox marketplace — your plugin appears in the sandbox marketplace once submitted.
  2. Fire a test event — use Vendor dashboard → Sandbox → Fire test event to send a synthetic ride.ended to your endpoint. Verify your signature check passes and your handler does the right thing.
  3. Test the config form — does it render correctly? Are validation errors clear?
  4. Test OAuth (if applicable) — does the authorize_url redirect correctly? Does the token_url accept the auth code?
  5. 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.pii but 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