Build a complete Salla app by performing the actions, not just describing them. Each step calls a Salla Partners MCP tool to do the work. Follow the steps in order — complete each gate before moving to the next.
Grounding rules — read the real value, confirm the real result.
- Verify the deployed domain before writing ANY URL into Salla. Read the live Vercel project domain —
vercel project ls, the Vercel MCP, or.vercel/project.json— before you setapp_url, an embeddediframe_url, or a snippet BASE. A guessed*.vercel.appthat doesn't resolve breaks install, webhooks, and the iframe silently (no error at write time).- Read back after every mutate.
connect/update/subscribecan return a minimal or empty body — a zero-field response is indistinguishable from a silent failure. Confirm with aget/listbefore treating the change as done; never trust the write response alone.- On a domain change, update every place the domain lives, together — portal
app_url, embeddediframe_url, snippet BASE, and every env var. Checklist: references/domain-consistency.md.
The arc: create → configure → publish. Creating the app is only the first gate — a created app is not published; it still needs scopes, webhooks/events, any UI, then review before it reaches merchants (docs.salla.dev/421410m0.md). The home for all of this is the Salla Partners account (verified) → portal.salla.partners → My Apps (docs.salla.dev/421412m0.md). The MCP tools below drive that same Portal, so prefer them when connected.
Tools
These steps drive the Salla Partners MCP tools. Each is one tool with an action:
| Tool | What it does |
|---|---|
salla_reference |
Look up categories, countries, cities |
salla_upload |
Upload a logo/file → returns a file id |
salla_apps |
create / update / get / list / connect (OAuth+webhooks) / set_status / demo_stores (testing). Public-app publishing uses the separate app_publish tool; private apps use salla_private_apps. |
salla_private_apps |
Private apps: status (no app_id = account kind) / publish (confirm-gated) / withdraw / store requests (list_requests get_request create_request update_request delete_request) → references/private-apps.md |
salla_scopes |
get valid scope slugs (+ disabled / selected) / set selected scopes (flat slug → read | read_write | "") |
salla_events |
list subscribable events / subscribe an app to slugs |
Prerequisite: the Salla Partners MCP server must be connected (the tools above appear in your tool list). If it isn't, fall back to the Portal at https://portal.salla.partners and the inline manual notes. Run the OAuth/login flow if a tool returns "Salla session expired — reconnect".
Step 0 — Discover
Ask before starting:
- What does your app do? (brief description)
- App type: General / Communication / Shipping
- Visibility: Public (App Store) or Private (invite-only)?
These are two independent choices (docs.salla.dev/421410m0.md):
Public apps appear in the Salla App Store for any merchant
to browse, download, or purchase; Private apps are built for specific merchants and
never surface in the store's listings or search. Category (General vs Shipping) is the
separate axis — an app's type is a single value (app / private / shipping /
communication), so a Shipping app is always Public, and Communication apps are typically
Public. Visibility is Portal-enforced per type, so let create/publish validate
the combination rather than assuming it.
Use the answers to tailor Steps 1, 4–7.
Step 1 — Create the App
- Resolve the category. Call
salla_referencewithaction: "categories"and thetype("app"or"shipping") to getsub_categories— pick create'ssub_category_idfrom there (a non-matching sub-category is rejected). Private apps usetype: "app"here —"private"is not a validsalla_referencecategory type. Forapp/shipping,sub_category_idis required at create (forappthe choices are POS, OMS, Subscription, Cross-sell/Upsell, Manage Store, AI, Others). That same call also returnsmain_categories/categories— don't reach for them yet, they're a separate, type-independent "App Theme"/"App Impact" list for publish'smain_category_id/categories, not this step → salla-publication-consistency. - Upload the logo. Call
salla_uploadwith a public imagesource_url. The logo must be a square (1:1) image, ≥ 250×250 px — ensure the source image satisfies that before uploading. The result returns only{id, url}(no dimensions are echoed), so use the returnedid. If there's no logo at creation and an image-generation tool is available, generate one (1:1, ≥ 250×250) and upload it — full canonical recipe (and every other listing/publication image field + its dimensions) → salla-app-ui-builder. - Create the app. The basic info Salla requires at create is icon, name, category,
description, app website, and support email
(docs.salla.dev/421410m0.md); via the MCP these
map to the fields below. Call
salla_appswithaction: "create"and:
| Field | Requirement |
|---|---|
name + name_ar |
Salla expects the app name in Arabic, in plain letters with no diacritics/tashkeel (e.g. هريفاي without a vowel mark, rather than هرّفاي with a diacritic), and unique across Salla apps. Treat these as Portal-enforced — let create validate: submit, then act on the error (rename and resubmit if the name is taken or invalid) rather than pre-checking client-side. Confirm the exact constraints from the create response when in doubt. |
type |
from step 1 (private or a public category) |
short_description (+_ar) |
50–200 chars each — bilingual like name |
app_url |
The app's live URL — read it from the deployed Vercel project (Grounding rule 1), not a guessed *.vercel.app. This is the source domain; if it later changes, update it here too (Step 1 gate / domain-consistency checklist). |
email |
support email |
logo |
file id from salla_upload |
sub_category_id |
required when type is app / shipping |
is_paid |
required for a private app — pass private_app_is_paid from step 3a. |
3a. Private app only: before create, call salla_private_apps action=status with no
app_id and pass its private_app_is_paid as is_paid (merchant partners get "0";
regular partners "1" unless Salla granted free private apps). Rules →
references/private-apps.md.
The result returns the new app_id — carry it through every later step. Open the app in
the Partners Portal to view, configure, and test it:
https://portal.salla.partners/apps/{app_id} (substitute the returned id). Surface this
link to the user after every create. That App Details page is the hub for everything
the next steps configure — App Keys (Client ID/Secret, OAuth mode), Scope, Webhooks,
Trusted IPs, App Functions, Settings, Onboarding, Embedded Pages, Snippets, Custom Plans,
Testing, and Publishing (docs.salla.dev/421410m0.md).
Note on
salla_apps action=update: the Portal returns no body, so the tool echoes the fields you changed ({ app: { id, …changed, updated: true } }) as confirmation — it reflects your input, not the server's stored state. For a high-stakes change, still read back withsalla_apps action=getto confirm the value actually persisted.
Manual fallback: Portal → My Apps → Create App.
Gate: "App created — confirm the returned app_id (salla_apps action=get), and for
type: private, is_paid came from salla_private_apps action=status?" A
created app is not yet published (docs.salla.dev/421410m0.md);
keep going through configure → publish.
Red Flags — create
| Tempting thought | Why it's wrong |
|---|---|
"create was rejected — 'can't create more than N private apps'; the feature must be off." |
The account has no free private apps left (regular partners get none by default). Create it as paid: set is_paid: "1" on salla_apps action=create. |
Step 2 — OAuth, Scopes & Webhook Connection
Default to Easy Mode. Easy Mode (tokens via the
app.store.authorizewebhook, no callback) is the recommended default for every app — use it unless there's a concrete technical reason it can't work. Custom Mode (an OAuth/callbackcode exchange) is for local dev / Postman during development; shipping a published app on Custom Mode without a real, justified use case can get it rejected at review. Mode mechanics → salla-app-auth.
Configure OAuth and webhooks in one salla_apps action=connect call. First check the
app's valid scope slugs and current selection:
-
Call
salla_scopeswithaction: "get"and theapp_idto read the valid scope slugs, their current selection, and any per-appdisabledflags. (There is no scope-catalog reference endpoint —salla_scopesreads them from the app.) Least privilege: request only the minimum slugs the app needs, and preferreadoverread_writeunless the app actually writes — excessive scopes risk review delay/rejection. Adisabledoption isn't saved, so honour the flags fromget. On a merchant partner's private app,shippings,settlements,subscriptionsandcustomer_walletsare hidden (no allow-list exception); on a regular partner's private appshippingsis disabled unless Salla allow-lists the app. Restricted scopes aren't saved andsalla_scopes action=setlists them underdropped→ references/private-apps.md. -
Call
salla_appswithaction: "connect",app_id, and any of:scopes— map ofslug → "read" | "read_write"(e.g.{"orders": "read", "products": "read"}). Pass only the resource map here —offline_accessbelongs in the OAuth authorize URL, not in thescopesmap. (You can also adjust the selection on its own withsalla_scopes action=set.)redirect_urls— OAuth redirect URL(s). HTTPS-only; keep the allowlist tight (register only the exact callbacks you use).webhook_url— your webhook receiver (HTTPS-only, must authenticate inbound requests via the signature/token strategy below)webhook_security_strategy—"signature"(recommended) or"token"trusted_ips,webhook_headers
Partial failures come back under
_partial— re-apply only the failed pieces.
connect does not mint or rotate the webhook signing secret. Create or rotate it in the
Partner Portal (https://portal.salla.partners/apps/{app_id}); rotating there invalidates the
old value. Read the current secret with salla_apps action=get (the webhook_secret field)
and store it in a secret manager (never in source/repo); it verifies the HMAC-SHA256 signature
on every webhook. Read it live right before deploy — never reuse one carried across sessions.
Signature verification + idempotency → salla-webhooks skill. Token handling
(Easy vs Custom mode, storage, refresh) → salla-app-auth skill. (Route, don't
reimplement here.)
Gate: "Scopes + redirect + webhook applied (no _partial), and a read-back
(salla_scopes action=get / salla_apps action=get) confirms the scopes, redirect, and
webhook URL actually stuck (Grounding rule 2). The webhook URL is the live deployed domain
(Grounding rule 1), returning 200, with the secret stored?"
Step 3 — Store Events Subscription
- Call
salla_eventswithaction: "list"andapp_idto get the valid event slugs the app can subscribe to (always call this first — slugs are validated). - Ask: "Which domains does your app react to?" Subscribe only to what's needed by
calling
salla_eventswithaction: "subscribe",app_id, andevents: [...slugs].
Hookable rule — App Functions first. BEFORE subscribing a webhook for a STORE event (order / product / cart / customer …), check the App Function trigger catalog (
salla-app-functions) and prefer an App Function: it runs inside Salla, readscontext.settings, and calls the Salla API without your own token/refresh plumbing or signature verification. Use webhooks only for lifecycle/auth events with no trigger —app.store.authorize(delivers Easy-Mode tokens), install/uninstall, trial/subscription. The clean split: lifecycle/auth → webhook · store automation → App Function · storefront UI → snippet · merchant config → embedded dashboard.
Common slugs by domain:
| Domain | Key events |
|---|---|
| Lifecycle (always) | app.store.authorize, app.installed, app.uninstalled, app.updated, app.subscription.started |
| Orders | order.created, order.updated, order.status.updated |
| Products | product.created, product.updated, product.deleted |
| Customers | customer.created, customer.updated |
| Shipments | shipment.created, shipment.cancelled, shipment.updated (async webhooks) — shipment.creating/shipment.cancelling are sync App Functions (see salla-shipping-app) |
A
webhook_urlmust be set (Step 2) before events will deliver. Unknown slugs are rejected with the valid list — pick from it (salla_events action=listis the source of truth).
Gate: "Subscribed. Trigger one event from the demo store and confirm your webhook receives it."
Step 4 — Storefront Snippets
Ask: "Does your app need to inject HTML/JS into the merchant's storefront?"
- Yes → follow the
salla-snippetsskill (it usessalla_snippetsto create the snippet). - No → skip to Step 5.
Step 5 — Embedded App Pages
Ask: "Does your app need a custom UI inside the Salla merchant dashboard?"
- Yes → follow the
salla-embedded-appskill (it usessalla_embedded_pagesto register the iframe page, plus SDK setup, auth, and theme sync). - No → skip to Step 5a.
Step 5a — Post-Install Onboarding Steps (Optional)
Ask: "Does your app need guided setup steps shown to the merchant right after install?" The onboarding flow is optional and, when present, runs once per merchant on their first install. Common use cases: collecting credentials (e.g. email + password) before the app activates, gathering store profile info, or configuring settings that cannot be changed later.
- Yes → each step is two mandatory parts, built in order: FIRST the step (the form, with
non-empty
fields), THEN its App Function handler. Create the step withsalla_onboarding_steps action=create(icon,title,slugall required —titleis a single-language plain string, NOT{ar,en}; a step has nourl;fieldsrequired, same schema as public app settings;sort,requiredoptional),action=sortto reorder them (change their display order),action=list/deleteto manage. Then — after confirming the step exists withaction=list(the trigger resolves from the saved step, so saving the handler first returns "Unknown trigger") — add the handler withsalla_functions action=save(triggerapp.onboarding.step.creating.{slug}, contextOnboarding): the merchant's input arrives ascontext.payload.data.fieldsto validate or run custom logic; returnResp.success()(continue) orResp.error().setFields(...)(stop + show feedback). The handler must be re-entrant — it fires on every submit (the merchant can edit and re-save before activating), so upsert and re-validate each run. Validate credentials provider-side, store only encrypted/hashed. Full tool params, the hard rules, theupdaterevalidation rule, the handler, and the completion payload shape: load references/onboarding-steps.md. - No → skip to Step 6.
Step 6 — App Functions
Ask: "Does your app need serverless handlers triggered by Salla events?"
- Yes → follow the
salla-app-functionsskill for the App Function source, context shape,RespAPI, and timeouts. App Functions handle store-event automation (where a trigger exists); lifecycle/auth events stay on webhooks (Step 3, owned bysalla-app-lifecycle/salla-app-auth).
Save the function with salla_functions action=save (app_id, trigger, content,
name) — an upsert (create or update). A saved function is live on the app's demo
stores immediately; it reaches real stores only after the app is published (Step 8). Read
with salla_functions action=get, remove with action=delete. (salla_functions is
operator-gated: it errors clearly if the App Builder service is not enabled on the MCP
deployment.) Details → salla-app-functions.
Gate: "Function saved and working on a demo store?" (Publishing to production is the later dedicated publish step — not here.)
Step 7 — App-Type-Specific Settings
Branch on the app type from Step 0:
General App
Needs per-merchant config (API keys, toggles, URLs)? → follow the
salla-app-settings skill (it uses salla_settings action=define_form).
Communication App
Sends messages on behalf of merchants (WhatsApp, SMS, email):
- Create with
type= the communication category — nosub_category_idfor communication apps. - Publish blocker: you must declare supported features via
salla_settings action=set_features(sms_local,sms_international,email_all,whatsapp) before publishing — submitting without them returns 403. - Full flow (channels, payloads, delivery status) →
salla-communication-appskill.
Shipping App
Integrates a carrier or fulfillment provider:
- Typically Public; if you target Private, let
create/publish validate the visibility rather than assuming it is allowed. - Follow the
salla-shipping-appskill (it usessalla_shippingfor zones/settings andsalla_appsfor the full lifecycle).
Step 8 — Test, Validate the Draft & Hand Off to the Partner
Public app vs Private app — how each publishes
Decide the path by app type before publishing — they do not share a flow:
- If
typeisprivate(installed only by specific merchant(s) via a store request) →salla_private_apps:status→publish(confirm: true, only after the partner confirms — a merchant partner's app usually goes live at once) →create_requestper store. There is no onboarding, no public listing, and no readiness sections — skip theapp_publishflow below entirely. Full flow → references/private-apps.md. - Else (a public app — App Store, any merchant can discover/install) → the stepwise
app_publishonboarding in sub-steps 1–4 below:open→ guidedsetper section →app_publish action=validate(validates + saves a DRAFT) → guide the partner to submit one-click in the Portal. This needs the full public listing (categories, pricing, screenshots, benefits, contact, etc.). Mechanics → follow salla-publication-consistency.
Gate: "Is type private? → salla_private_apps action=status app_id → the partner
explicitly confirmed → publish with confirm: true → once the publication is approved
(a regular partner's goes to Salla review first), create_request per store
(references/private-apps.md), then STOP — no app_publish,
no onboarding. Otherwise continue with the public app_publish flow below."
-
Test on a demo store. List the company's demo stores with
salla_apps action=demo_stores,app_id. Each store returns:connected—truemeans the app is already installed on that store.install_url— open in a browser to install the app on that store.dashboard_url— auto-login to that store's admin (to open the embedded dashboard, change settings, etc.).url— storefront preview (to verify snippets/urgency signals on product pages).
Pick a store, open its
install_urlto install, thendashboard_urlto manage it, and trigger each subscribed event to verify end-to-end behavior. Surface these links to the user. You can also open the app itself in the Portal:https://portal.salla.partners/apps/{app_id}. -
Move the app to live when ready:
salla_apps action=set_status,status: "live". -
Validate + save the draft. The agent's terminal publish action is validate, not submit — it validates every section, saves a DRAFT, and stops there. Use the guided path:
- Primary — guided, stepwise
app_publish:open→ (set<section>→readiness)* →validate.opencreates the draft (and unlocksapp_page_builderfor the listing page); then for each of the 5 sections (basic_information,features,pricing,contact_information,service_trial) callset, re-checkreadiness, and fix one section at a time off the returnedmissinglist until every section readscomplete; then runapp_publish action=validateto validate and save the draft. First-time publish is a guided onboarding, not a blind fill: the sections carry the partner's business decisions (listing copy, categories, pricing, contact) — ask the partner per section, suggest Salla-grounded options, and fill from their answers; never auto-invent them. Section fields, the guided-onboarding rule, the listing-image rule, and the Portal hand-off → salla-publication-consistency (follow it for the mechanics). The same server-side gate (app_publish action=validate) runs and returns 422 with the still-missing sections if it isn't ready. Listing content (name/description/logo/screenshots/benefits) is written viaapp_page_builder→ salla-app-ui-builder; plan/addon pricing → salla-app-billing.
- Primary — guided, stepwise
-
Partner reviews, then send the publish request.
validateonly saves a DRAFT. After a clean validate, give the partner their real/publishlink with the app's actual id substituted (never the placeholder):https://portal.salla.partners/apps/{app_id}/publish(e.g..../apps/1234567/publish) and ask them to review the draft there. It goes to Salla review only either when they submit one-click in the Portal or, after they explicitly confirm, when you callapp_publish action=send_publish_request(confirm: true) — never before review + confirmation. Once submitted and approved it's live on https://apps.salla.sa/en. Mechanics → salla-publication-consistency.
Testing guide: references/demo-store-testing.md
Gate: "Sections validated + saved as a draft, the partner reviewed the real /publish
link, and the publish request is sent only on their one-click submit or explicit confirmation?"
Red Flags — publishing
| Tempting thought | Why it's wrong |
|---|---|
"It's a private app, I'll run the public app_publish onboarding to publish it." |
Private apps don't use the stepwise listing flow. Publish with salla_private_apps action=publish (confirm: true after the partner confirms) — no sections, no onboarding, no listing. |
"I'll app_publish action=submit / push the private app through the readiness gate." |
A private app has no public listing to validate, so the public validate gate doesn't apply. Use salla_private_apps, then send it to stores with create_request. |
"app_publish action=validate passed — the public app is now submitted for review." |
validate only validates and saves a DRAFT; it does not submit. Give the partner the real /publish link (.../apps/{app_id}/publish); review reaches Salla only on their one-click submit or, after explicit confirmation, send_publish_request (confirm:true). |
Resources
| Topic | Link |
|---|---|
| Get Started | https://docs.salla.dev/421412m0.md |
| Create Your First App | https://docs.salla.dev/421410m0.md |
| Partners Portal | https://portal.salla.partners/ |
| Apps Marketplace | https://apps.salla.sa/en |
| Webhooks guide + event list | https://docs.salla.dev/421119m0.md |
| App Events (lifecycle) | https://docs.salla.dev/421413m0.md |
| Salla Admin API reference | https://docs.salla.dev/421117m0.md |
| Developer community (Telegram) | https://t.me/salladev |