MojoShop

eCommerce Exchange API

Versioned catalog, cart, checkout, customer account, and partner endpoints for the Exchange. MojoShop is the reference storefront; institutions can whitelabel against the same /api/v1 contract. Public catalog payloads never include vendor identity.

Contract

Send JSON. Use Accept: application/json and Content-Type: application/json on POST, PATCH, and DELETE. Successful payloads wrap in data. Lists that paginate also include Laravel links and meta (page, per_page, total).

Customer auth: Authorization: Bearer {token} from login or register (Sanctum token named storefront). Partners: X-Api-Key. Carts: send X-Cart-Token and read it back on every cart response.

Checkout and partner orders accept optional idempotency_key (max 255). Keys are scoped per shopper, guest email, or partner API client so one client cannot replay another’s key. Reuse the same key to get the original order instead of creating another. Empty carts and stock problems return 422 with an errors.cart array.

Rate limits: 60 requests per minute per IP or customer by default. Register 10/min, login and reviews 20/min, password reset and OTP resend 6/min, checkout 30/min, guest order lookup 60/min, partner orders 30/min. Over the limit returns 429 with Retry-After.

Errors

401 Missing or invalid Bearer token, or missing/invalid X-Api-Key (partners may see a more specific message).

{ "message": "Unauthenticated." }

403 Authenticated but not allowed (non-customer token, or partner key without external payment).

{ "message": "This endpoint is for customer accounts." }

404 Unknown product, cart item, or order number + email pair.

{ "message": "Not found." }

422 Validation failed, empty cart, unavailable variant, not enough stock, or delivery quote failed.

{ "message": "…", "errors": { "field": ["…"] } }

429 Rate limit exceeded. Retry-After is set. Default 60/min; register, login, password reset, checkout, and partners are tighter.

{ "message": "Too Many Attempts." }

502 MojoPay rejected the checkout session.

{ "message": "Could not create a MojoPay checkout session: …" }

503 MojoPay or Swoove Delivery is not configured in /admin.

{ "message": "Delivery is temporarily unavailable. …" }

Catalog

GET /api/v1/products Public

List published products. Query: q (max 200), category_id, min_price, max_price, in_stock, on_sale, min_rating (0–5), sort (newest|popular|rating|price_asc|price_desc|name), per_page (1–48, default 24), page. Invalid filters return 422. Includes rating_avg, rating_count, sold_count, discount_percent, and pagination meta/links. Soft anonymity — no vendor identity.

GET /api/v1/products/{id} Public

Product detail with variants, images, related, and recent anonymous reviews. Unpublished IDs return 404.

GET /api/v1/products/{id}/related Public

Related products in the same category.

GET /api/v1/products/{id}/reviews Public

Paginated approved reviews (20 per page). Soft anonymity — labelled Verified buyer, no names.

POST /api/v1/products/{id}/reviews Bearer

Leave a 1–5 star review after buying the product. Body: rating, optional body. Returns 201 with the review and updated product.

GET /api/v1/categories Public

Active categories (id, parent_id, name, slug, description, sort_order).

GET /api/v1/flash-banner Public

Permanent header promo slides: staff promo lines, featured products, and sale items. Soft anonymity — no vendor identity.

Cart

GET /api/v1/cart Optional Bearer

Get or create cart. Send and read header X-Cart-Token (also in data.token and meta.cart_token). Merges into the account cart when signed in.

POST /api/v1/cart/items Optional Bearer

Add item. Body: product_variant_id, quantity. Variant must be active on a published product. Response includes X-Cart-Token.

PATCH /api/v1/cart/items/{id} Optional Bearer

Update item quantity. Unknown items for this cart return 404.

DELETE /api/v1/cart/items/{id} Optional Bearer

Remove cart item. Unknown items for this cart return 404.

Checkout

POST /api/v1/checkout/shipping-quote Optional Bearer

Preview Swoove delivery fee for the current cart. Requires customer_name, customer_email, customer_phone, and shipping_address. Returns data.shipping_total, currency, and anonymized legs (no seller names). 503 when Delivery is not configured.

POST /api/v1/checkout Optional Bearer

Create order from cart and initiate Mojo Omni payment. Body requires shipping_method (delivery|pickup), accept_terms, and optional idempotency_key. Delivery re-quotes Swoove server-side and adds shipping_total to grand_total (phone + Ghana shipping_address required). Pickup has zero shipping; anonymized pickup addresses appear on order lookup after pay. Returns 201 with data.order + data.payment.checkout_url.

Orders

GET /api/v1/orders/{number} Public

Guest order lookup. Query: email (must match checkout email, case-insensitive). Includes shipping_method, shipping_total, fulfillment status, anonymized pickup blocks when shipping_method is pickup, and tracking when a Swoove shipment exists. No seller identity.

Account

POST /api/v1/auth/register Public

Create a customer account (phone required). Sends email + SMS OTP when Postmark/Hubtel are connected. Returns 201 with token + user (needs_verification when codes are pending).

POST /api/v1/auth/login Public

Customer login. Returns Sanctum token. Resends OTPs when the account still needs verification. Vendor/admin accounts must use their portals (422).

POST /api/v1/auth/verify Bearer

Confirm email_code and/or phone_code (6 digits). Customer tokens only.

POST /api/v1/auth/resend-verification Bearer

Resend OTP. Body: channel = email | phone | both.

POST /api/v1/auth/forgot-password Public

Email a shop password reset link when Postmark is connected. Always returns the same message.

POST /api/v1/auth/reset-password Public

Set a new shopper password with the emailed token. Body: email, token, password, password_confirmation.

POST /api/v1/auth/logout Bearer

Revoke the current storefront token.

GET /api/v1/auth/me Bearer

Current customer profile. Non-customer tokens return 403.

PATCH /api/v1/auth/me Bearer

Update name, email, phone, and optional shipping_address (Ghana region required when country is GH). Clear by omitting address lines.

GET /api/v1/account/orders Bearer

Paginated order history for the signed-in customer (20 per page).

Partners

POST /api/v1/external/orders X-Api-Key

Create a paid order using a third-party payment reference. Requires shipping_method (delivery|pickup), accept_terms, and Ghana shipping_address.region when delivering. Optional idempotency_key is scoped per API client. Response shape matches checkout: data.order + data.payment.

Vendor webhooks

Vendors enter an HTTPS callback URL in the vendor panel at /vendor (Webhooks). There is no API endpoint to register or change webhook URLs. MojoShop POSTs JSON to your URL when events happen. Respond with 2xx; failed deliveries retry a few times.

Headers: X-MojoShop-Event, X-MojoShop-Timestamp, X-MojoShop-Signature (HMAC-SHA256 of {timestamp}.{raw_body} using your signing secret).

POST order.paid Your URL

Customer payment confirmed. JSON includes the order (your customer + shipping) and only your fulfillment and line items.

{
  "event": "order.paid",
  "occurred_at": "2026-08-20T19:00:00+00:00",
  "data": {
    "order": {
      "number": "MS-ABCDEF1234",
      "status": "paid",
      "customer_name": "Ama Buyer",
      "customer_email": "ama@example.com",
      "customer_phone": "0240000000",
      "shipping_address": {
        "line1": "12 Independence Ave",
        "city": "Accra",
        "region": "greater_accra",
        "country": "GH"
      },
      "currency": "GHS",
      "paid_at": "2026-08-20T19:00:00+00:00"
    },
    "fulfillment": {
      "number": "FF-ABCDEF1234",
      "status": "pending",
      "subtotal": 80.00,
      "vendor_amount": 72.00,
      "items": [
        {
          "product_name": "Shea Butter",
          "variant_name": "Jar",
          "sku": "SHEA-JAR",
          "quantity": 2,
          "unit_price": 40.00,
          "line_total": 80.00
        }
      ],
      "shipment": null
    }
  }
}

POST fulfillment.packed Your URL

Your packing list was marked packed (or a Swoove delivery was booked). Full order/fulfillment shape (same customer + shipping fields as order.paid); shipment may include tracking.

{
  "event": "fulfillment.packed",
  "occurred_at": "2026-08-20T19:10:00+00:00",
  "data": {
    "order": {
      "number": "MS-ABCDEF1234",
      "status": "partially_fulfilled",
      "customer_name": "Ama Buyer",
      "customer_email": "ama@example.com",
      "customer_phone": "0240000000",
      "shipping_address": {
        "line1": "12 Independence Ave",
        "city": "Accra",
        "region": "greater_accra",
        "country": "GH"
      },
      "currency": "GHS",
      "paid_at": "2026-08-20T19:00:00+00:00"
    },
    "fulfillment": {
      "number": "FF-ABCDEF1234",
      "status": "packed",
      "subtotal": 80.00,
      "vendor_amount": 72.00,
      "items": [
        {
          "product_name": "Shea Butter",
          "variant_name": "Jar",
          "sku": "SHEA-JAR",
          "quantity": 2,
          "unit_price": 40.00,
          "line_total": 80.00
        }
      ],
      "shipment": {
        "provider": "swoove",
        "status": "label_created",
        "tracking_number": "SD-123456",
        "carrier": "swoove",
        "shipped_at": null,
        "delivered_at": null
      }
    }
  }
}

POST fulfillment.shipped Your URL

Parcel is in transit. Same full order/fulfillment payload shape as order.paid; shipment status and tracking are populated when available.

{
  "event": "fulfillment.shipped",
  "occurred_at": "2026-08-20T19:30:00+00:00",
  "data": {
    "order": {
      "number": "MS-ABCDEF1234",
      "status": "partially_fulfilled",
      "customer_name": "Ama Buyer",
      "customer_email": "ama@example.com",
      "customer_phone": "0240000000",
      "shipping_address": {
        "line1": "12 Independence Ave",
        "city": "Accra",
        "region": "greater_accra",
        "country": "GH"
      },
      "currency": "GHS",
      "paid_at": "2026-08-20T19:00:00+00:00"
    },
    "fulfillment": {
      "number": "FF-ABCDEF1234",
      "status": "shipped",
      "subtotal": 80.00,
      "vendor_amount": 72.00,
      "items": [
        {
          "product_name": "Shea Butter",
          "variant_name": "Jar",
          "sku": "SHEA-JAR",
          "quantity": 2,
          "unit_price": 40.00,
          "line_total": 80.00
        }
      ],
      "shipment": {
        "provider": "swoove",
        "status": "in_transit",
        "tracking_number": "SD-123456",
        "carrier": "swoove",
        "shipped_at": "2026-08-20T19:30:00+00:00",
        "delivered_at": null
      }
    }
  }
}

POST fulfillment.delivered Your URL

Parcel marked delivered. Same full order/fulfillment payload shape as order.paid.

{
  "event": "fulfillment.delivered",
  "occurred_at": "2026-08-20T20:00:00+00:00",
  "data": {
    "order": {
      "number": "MS-ABCDEF1234",
      "status": "fulfilled",
      "customer_name": "Ama Buyer",
      "customer_email": "ama@example.com",
      "customer_phone": "0240000000",
      "shipping_address": {
        "line1": "12 Independence Ave",
        "city": "Accra",
        "region": "greater_accra",
        "country": "GH"
      },
      "currency": "GHS",
      "paid_at": "2026-08-20T19:00:00+00:00"
    },
    "fulfillment": {
      "number": "FF-ABCDEF1234",
      "status": "delivered",
      "subtotal": 80.00,
      "vendor_amount": 72.00,
      "items": [
        {
          "product_name": "Shea Butter",
          "variant_name": "Jar",
          "sku": "SHEA-JAR",
          "quantity": 2,
          "unit_price": 40.00,
          "line_total": 80.00
        }
      ],
      "shipment": {
        "provider": "swoove",
        "status": "delivered",
        "tracking_number": "SD-123456",
        "carrier": "swoove",
        "shipped_at": "2026-08-20T19:30:00+00:00",
        "delivered_at": "2026-08-20T20:00:00+00:00"
      }
    }
  }
}