Developer docs
How to build, submit and get paid for themes and plugins on Wcart.
Both use the same steps below; a full integration simply uses more of the API. (“Apps” on Wcart means mobile apps, which are separate.)
Steps
- Create the listingOn New listing, choose Plugin.
- DetailsDescription, screenshots, category and your price (in your currency).
- ManifestFill in the manifest builder on the Build step: name, settings fields, webhook URL and/or embedded page URL, and scopes. It is checked as you save.
- OAuth credentialsCreate them on the same step with your redirect URL (the fallback). You get a
client_idand aclient_secret; the secret is shown once. Your server needs both to accept installs — see How merchants get your plugin. - Test installInstall it on your dev store from the Test install step. You get an access token (
wca_…) once; your plugin calls the API with it. A test install may use every scope you request, and also shows the webhook signing secret once. - Test orderDev stores cannot take real orders. Press Place test order next to the install (or
POST /dev-stores/:storeId/test-order) to fireorder.createdat your webhook. - SubmitSubmit a version. The reviewer sees the manifest, settings and URLs and decides which scopes to grant.
- LiveAfter approval Wcart prices it in its billing system; a priced plugin goes live for every store at once and appears on every merchant’s Plugins page.
The manifest
{
"name": "Reviews Sync",
"description": "Pull reviews from …",
"settings_schema": [
{ "key": "api_key", "label": "API key", "type": "text", "required": true },
{ "key": "mode", "label": "Mode", "type": "select", "options": ["live", "test"] },
{ "key": "auto_publish", "label": "Publish automatically", "type": "boolean" }
],
"embedded_url": "https://your-plugin.example/embed",
"webhook_url": "https://your-plugin.example/webhooks/wcart",
"scopes": ["orders:read"]
}- Field types:
text,number,boolean,select(withoptions); up to 40 fields. Keys are lowercase letters, digits and_. - At least one of
webhook_urlandembedded_url. They must be public https URLs — Wcart calls them from its servers. To test a plugin running on your own machine, expose it with a tunnel (ngrok, cloudflared) and use that https address. - Also request the same scopes on the listing’s Details step: review grants from the listing’s requested scopes.
- A manifest cannot name a React
component.
Settings: what the merchant fills in
After installing, the merchant opens your plugin from Plugins → Manage in their Wcart admin and fills in the form your settings_schema describes (required fields are marked, selects and numbers are checked). Read the values from your server with the install’s token whenever you need them:
GET https://developer-api.wcart.io/app/v1-settings
Authorization: Bearer wca_…
→ { "store_id": "…", "settings": { "api_key": "…", "mode": "test" },
"complete": true, "updated_at": "…", "form": [ … ] }Defaults from your form fill any value the merchant has not set. complete is false until every required field is filled; show a “finish setup” message until then. On a test install the form comes from your saved draft, so you can try it before review; on a merchant install it is the approved version’s.
Your page inside the Wcart admin
If your manifest has an embedded_url, the merchant sees Open plugin and your page loads in a frame inside their admin. Wcart adds ?wcart_embed=…&store_id=…&plugin=…. Never trust store_id from the URL: verify the embed token from your server first:
POST https://developer-api.wcart.io/oauth/embed/verify
{ "client_id": "wcapp_…", "client_secret": "wcsec_…", "embed_token": "wcemb_…" }
→ { "store_id": "…", "plugin": "…", "install_id": "…", "scopes": [...],
"user_email": "…", "expires_at": "…" }The token is valid for 10 minutes; start your own session once it is verified. Your page must allow being framed by the Wcart admin (do not send X-Frame-Options: DENY, or allow frame-ancestors for admin.wcart.io). The merchant can also open it in a new tab.
Scopes
Request scopes in the manifest; review decides which are granted, and an install only ever holds those. A test install on your own dev store gets every scope you request. :write includes :read. Sensitive scopes (money, personal data, outbound email) need a reason in your review notes.
products:read— Read products, categories, brands and tagsproducts:write— Create and update products, categories, brands, tags and attributesinventory:read— Read stock levels and pickup locationsinventory:write— Adjust stock
orders:read— Read orders and their line itemsfulfillments:write— Add shipment tracking to an orderpayments:read— Read payment transactions of orderspayments:write— Mark an order as paid (payment-gateway plugins) · sensitive
customers:read— Read customer records · sensitivecustomers:write— Create guest customer records · sensitive
store:read— Read store address, currencies, tax settings and locations
reviews:read— Read product reviews and rating summaries · data exists only on stores using Ratings & Reviewsbarcodes:read— Look up a product by barcode · data exists only on stores using Product Barcodememberships:read— Read a customer’s membership benefits and product access · data exists only on stores using Membershipswallet:write— Credit, debit and reverse customer store credit · sensitive · data exists only on stores using Walletreward_points:write— Award, redeem and reverse reward points · sensitive · data exists only on stores using Reward Pointsgift_cards:write— Issue gift cards · sensitive · data exists only on stores using Gift Cards
notifications:send— Send an email to a customer through the store’s mail settings · sensitive
webhooks:manage— Receive the store’s events (orders, products) as signed webhooks
Calling Wcart: the API gateway
Send the install’s token as a bearer token to https://developer-api.wcart.io. Every route, with the scope it needs:
GET /app/v1-whoami → your plugin, store and scopes GET /app/v1/products products:read List published products (?page, limit, q, category) GET /app/v1/products/by-ids products:read Read products by id (?ids=a,b) GET /app/v1/products/slug/:slug products:read Read one published product by slug GET /app/v1/categories products:read List categories GET /app/v1/brands products:read List brands GET /app/v1/tags products:read List tags POST /app/v1/products products:write Create a product (product_name, sku, price, sale_price, quantity, status) PUT /app/v1/products/:id products:write Update a product (any of the create fields) POST /app/v1/categories products:write Create a category (name) POST /app/v1/brands products:write Create a brand (name) POST /app/v1/tags products:write Create a tag (name) POST /app/v1/attributes products:write Create an attribute (name) GET /app/v1/inventory/stock inventory:read Stock for products (?ids=a,b) GET /app/v1/inventory/pickup-locations inventory:read Store pickup locations POST /app/v1/inventory/adjust inventory:write Adjust stock (product_id, delta) GET /app/v1/orders/by-users orders:read Orders of customers (?user_ids=a,b) GET /app/v1/orders/:id orders:read Read one order with its items (each item gets product_name) POST /app/v1/orders/:id/tracking fulfillments:write Add tracking (carrier, tracking_number, tracking_url) GET /app/v1/payments/by-orders payments:read Transactions of orders (?order_ids=a,b) GET /app/v1/payments/by-reference payments:read Transaction by gateway reference (?reference=) POST /app/v1/orders/:id/paid payments:write Mark an order paid (transaction reference) GET /app/v1/customers/search customers:read Find customers (?q=email or phone) GET /app/v1/customers/by-ids customers:read Read customers by id (?ids=a,b) POST /app/v1/customers/guest customers:write Find or create a guest customer (email, name, phone) GET /app/v1/store/address store:read Store address GET /app/v1/store/currency store:read Default currency GET /app/v1/store/tax store:read Tax settings GET /app/v1/store/locations store:read Store locations GET /app/v1/store/currencies store:read All enabled currencies with rates GET /app/v1/reviews reviews:read Approved reviews of a product (?product_id=) GET /app/v1/reviews/summary reviews:read Rating summary of a product (?product_id=) GET /app/v1/barcodes/lookup barcodes:read Product for a barcode (?code=) GET /app/v1/memberships/member-benefits memberships:read Benefits of a customer (?customer_id=) GET /app/v1/memberships/product-access memberships:read Can a customer see a product (?customer_id, product_id) POST /app/v1/wallet/credit wallet:write Credit store credit (customer_id, amount, idempotency_key) POST /app/v1/wallet/debit wallet:write Debit store credit (customer_id, amount, idempotency_key) POST /app/v1/wallet/reverse wallet:write Reverse a wallet entry (idempotency_key) POST /app/v1/rewards/earn reward_points:write Award points POST /app/v1/rewards/redeem reward_points:write Redeem points POST /app/v1/rewards/reverse reward_points:write Reverse a points entry POST /app/v1/gift-cards gift_cards:write Issue a gift card (amount, recipient_email) POST /app/v1/notifications/email notifications:send Email a customer (to, subject, body, heading) Authorization: Bearer wca_…
Lists (/products, /categories…) return what the storefront shows; by-id reads see everything in the store. Creating a product needs product_name and a unique sku. Order line items carry productid — read names with /products/by-ids.
The store comes from the token, never the request. Anything not listed answers 404 NOT_ALLOWED; a call without its scope answers 403 MISSING_SCOPE.
How merchants get your plugin
Merchants never visit this portal. Everything happens in their Wcart admin → Plugins, where your plugin is listed with Wcart’s own (with “by your organisation” under its name). Wcart bills them and pays you.
- DiscoverIn the admin’s Plugins page, or in the marketplace — whose button sends them to their admin.
- BuyA paid plugin shows Buy Now · price and goes through Wcart’s checkout. A free one, or one included in the store’s plan, skips this.
- ApproveThey press Activate, see the access review granted you, and press Approve & activate. Nothing is granted before that.
- Wcart tells your serverWcart POSTs a signed
app.installedevent to your install URL (manifestinstall_url, else yourwebhook_url) with a one-timecode. - You exchange the codeYour server swaps it (single use, valid 10 minutes) for that store’s token — before answering 200, so the merchant sees “connected” straight away.
- Set upThe admin opens your plugin’s page: your settings form and your embedded page. Anything you need from the merchant (signing in to Google, an API key of theirs) happens there, never on a separate site.
POST <your install URL> ← from Wcart
X-WCart-Event: app.installed
X-WCart-Timestamp: 1791358411629 (unix ms)
X-WCart-Signature: <hex HMAC-SHA256 of "<timestamp>.<raw body>">
{ "id": "evt_…", "event": "app.installed",
"payload": { "store_id": "dep_…", "plugin": "<your slug>", "install_id": "…",
"code": "wccode_…", "scopes": [...], "mode": "live" } }
signing key = SHA-256 (hex) of YOUR client secret
POST https://developer-api.wcart.io/oauth/token → to Wcart
{ "client_id": "wcapp_…", "client_secret": "wcsec_…", "code": "wccode_…" }
→ { "access_token": "wca_…", "store_id": "dep_…", "scopes": [...],
"webhook_signing_secret": "whsec_…" }If your server cannot be reached, Wcart retries for about 4 minutes and offers the merchant a “Finish setup” link to your redirect URL (?code=…&store_id=…&plugin=…) — keep that route working as the fallback. On deactivation Wcart revokes the token, removes the webhook subscription and POSTs a signed app.uninstalled ({ store_id, plugin, install_id }) to the same URL.
Activation rules
| Situation | What happens |
|---|---|
| Your organisation is not approved yet | You cannot create listings. Set up your dev store and payout details meanwhile. |
| Your own dev store | Test install from the listing’s Test install step — free, every scope you request, token shown once. Other developers’ plugins cannot be installed there. |
| Paid plugin, store has not bought it | Activate is refused (402 PURCHASE_REQUIRED); the merchant is shown Buy Now. |
| Bought, included in the plan, or granted free by Wcart | Activate works. You earn in every case (see Earnings). |
| Merchant deactivates | Token revoked at once, app.uninstalled sent. Re-activating needs no new purchase and issues a new token. |
| Store expires or is suspended | Your API calls for it fail; keep the record, it may come back. |
| You publish a new version | Existing installs keep working. Asking for more scopes needs review, and merchants approve the new access. |
Only the store’s owner can activate or deactivate a plugin.
Running one plugin for many stores
One deployment of your plugin serves every store that activates it. Key everything by store_id.
| Keep per store | Where it comes from |
|---|---|
store_id | app.installed payload / token exchange |
access_token (encrypted at rest) | token exchange — for /app/v1/… calls |
webhook_signing_secret | token exchange — verifies that store’s order/product webhooks |
granted scopes, status (active / uninstalled) | token exchange; app.uninstalled |
| the merchant’s settings | GET /app/v1-settings (they edit them in their admin — do not copy them into your own form) |
- Which store sent this webhook? The one whose signing secret verifies the signature (or the
store_idin the payload, after verifying). - Which store is viewing my page? Verify the
wcart_embedtoken withPOST /oauth/embed/verify— it returns the store. Never trust astore_idin the URL alone. - Calling the API: use that store’s token. The store comes from the token, so one store’s token can never touch another store.
- Idempotency: install, webhook and uninstall events can arrive twice; use the event
id/install_idand upsert. - Re-install: a new
app.installedfor a store you know replaces its token and secret — overwrite, don’t duplicate. - Isolation: never mix two stores’ data in one query, file or cache key; log the
store_idwith every action.
The reference plugin Sheets Sync does all of this in one small file — ask Wcart for the source.
Webhooks
With a webhook URL, each install is subscribed to Wcart’s events (e.g. order.paid), signed with HMAC-SHA256 using the install’s signing secret, retried with backoff and audited. Verify the signature before trusting a payload. Uninstalling removes the subscription.
Guidelines
- Ask only for the scopes you use; reviewers grant the minimum.
- Keep the client secret and tokens on your server only, never in a browser or app bundle.
- Respond to webhooks quickly (2xx) and do slow work afterwards; handle the same event arriving twice.
- Test every flow on your dev store before submitting, including uninstall and reinstall.
- Answer
app.installedonly after the code exchange succeeds, within 8 seconds; reject a bad signature or a timestamp older than 5 minutes.