Skip to content
Last updated

Salla

An e-commerce platform with OAuth 2.0 auth, customer extraction, and order/cart handlers.

Legacy block in snippet

This example includes deprecated webhook.ecommerce for illustration. New submissions must use Sync (sync.resources + sync.webhooks.handlers).

const sallaMiniApp = {
  ns: 'salla',
  name: 'salla',
  type: 'salla',
  title: 'Salla',
  label: 'Salla',
  description: 'Sync orders, customers, and abandoned carts from Salla.',
  shortDescription: 'Salla ecommerce: orders, customers, carts',
  status: 'public',
  version: '1.0.0',
  category: 'ecommerce',
  icon: 'https://cdn.karzoun.com/miniapps/salla/icon.svg',
  logo: 'https://cdn.karzoun.com/miniapps/salla/logo.svg',
  docsUrl: 'https://docs.salla.dev',

  auth: {
    type: 'oauth2',
    sensitiveKeys: ['accessToken', 'refreshToken', 'webhookSecret'],
    config: {
      accessToken: '',
      refreshToken: '',
      webhookSecret: '',
    },
    userDetails: {
      url: 'https://api.salla.dev/admin/v2/oauth2/user/info',
      method: 'GET',
      headers: {
        Authorization: 'Bearer [[accessToken]]',
        'Content-Type': 'application/json',
      },
      mapping: { uid: '$.data.id', name: '$.data.name' },
    },
    get_token: {
      url: 'https://accounts.salla.sa/oauth2/token',
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      bodyType: 'json',
      body: {
        client_id: '{{client_id}}',
        client_secret: '{{client_secret}}',
        grant_type: 'authorization_code',
        code: '{{code}}',
        redirect_uri: '{{redirect_uri}}',
      },
      mapping: {
        accessToken: '$.access_token',
        refreshToken: '$.refresh_token',
        expiresIn: '$.expires_in',
      },
    },
    refresh_token: {
      url: 'https://accounts.salla.sa/oauth2/token',
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      bodyType: 'json',
      body: {
        client_id: '{{client_id}}',
        client_secret: '{{client_secret}}',
        grant_type: 'refresh_token',
        refresh_token: '[[refreshToken]]',
      },
      mapping: {
        accessToken: '$.access_token',
        refreshToken: '$.refresh_token',
      },
    },
    auto_refresh: true,
    registrationRequests: [],
  },

  triggers: [
    { type: 'miniapps:salla', event: 'order.created', icon: 'shopping-cart',
      label: 'Order Created', description: 'New order placed', isCustom: false, img: 'automation3.svg' },
    { type: 'miniapps:salla', event: 'order.status.updated', icon: 'shopping-cart',
      label: 'Order Status Changed', description: 'Order status updated', isCustom: false, img: 'automation3.svg' },
    { type: 'miniapps:salla', event: 'abandoned.cart', icon: 'shopping-cart',
      label: 'Cart Abandoned', description: 'Customer abandoned their cart', isCustom: false, img: 'automation3.svg' },
    { type: 'miniapps:salla', event: 'customer.created', icon: 'users-alt',
      label: 'Customer Created', description: 'New customer registered', isCustom: false, img: 'automation3.svg' },
    // ... more triggers
  ],

  actions: [],   // E-commerce apps typically have triggers only

  source: {},

  webhook: {
    // Event name is in the body
    eventExtraction: {
      source: 'body',
      path: '$.event',
    },

    // Dedup using data ID
    transactionId: {
      path: '$.data.id',
      fallback: 'generate',
    },

    // HMAC-SHA256 verification
    // Salla uses a global webhook secret (set in partner dashboard),
    // so the secret is stored in auth.config.webhookSecret (not per-tenant)
    verification: {
      type: 'hmac-sha256',
      headerName: 'X-Salla-Signature',
      secretKey: 'webhookSecret',    // Resolves from auth.config (global secret)
    },

    // Acknowledge immediately
    response: { statusCode: 200, body: { ok: true } },

    // Auto-extract customer from webhook payload
    customerExtraction: {
      basePath: '$.data.customer',
      mapping: {
        firstName: 'first_name',
        lastName: 'last_name',
        primaryEmail: 'email',
        primaryPhone: 'mobile',
        code: 'id',
      },
      overrides: {
        'customer.created': { basePath: '$.data' },
      },
    },

    // E-commerce data processing
    ecommerce: {
      platform: 'salla',

      // Pipeline stages
      stageMappings: {
        orders: {
          type: 'static',
          stages: [
            { name: 'Pending', code: 'pending', externalStatusCode: 'pending', probability: '10', order: 0 },
            { name: 'Processing', code: 'processing', externalStatusCode: 'processing', probability: '30', order: 1 },
            { name: 'Shipped', code: 'shipped', externalStatusCode: 'shipped', probability: '60', order: 2 },
            { name: 'Delivered', code: 'delivered', externalStatusCode: 'delivered', probability: '90', order: 3 },
            { name: 'Completed', code: 'completed', externalStatusCode: 'completed', probability: '100', order: 4 },
            { name: 'Cancelled', code: 'cancelled', externalStatusCode: 'cancelled', probability: '0', order: 5 },
          ],
        },
        abandonedCarts: {
          type: 'static',
          stages: [
            { name: 'Abandoned', code: 'abandoned', externalStatusCode: 'abandoned', probability: '10', order: 0 },
            { name: 'Recovered', code: 'recovered', externalStatusCode: 'recovered', probability: '50', order: 1 },
            { name: 'Completed', code: 'completed', externalStatusCode: 'completed', probability: '100', order: 2 },
          ],
        },
      },

      // Handler per webhook event
      handlers: {
        'order.created': {
          recordType: 'order',
          operation: 'create',
          mapping: {
            code: '$.data.id',
            orderNumber: '$.data.reference_id',
            status: '$.data.status.name',
            totalAmount: '$.data.amounts.total.amount',
            currency: '$.data.amounts.total.currency',
            paymentMethod: '$.data.payment_method',
            discountCode: { path: '$.data.discounts', transform: 'joinCodes' },
            items: { path: '$.data.items', transform: 'mapItems' },
          },
        },

        'order.status.updated': {
          recordType: 'order',
          operation: 'update',
          mapping: {
            code: '$.data.id',
            status: '$.data.status.name',
          },
        },

        'abandoned.cart': {
          recordType: 'abandonedCart',
          operation: 'create',
          mapping: {
            code: '$.data.id',
            totalAmount: '$.data.total.amount',
            currency: '$.data.total.currency',
            checkoutUrl: '$.data.checkout_url',
            state: 'abandoned',
            items: { path: '$.data.items', transform: 'mapItems' },
          },
        },
      },
    },
  },
};