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). 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 60/min. Over the limit returns 429 with Retry-After.

Errors

401 Missing or invalid Bearer token, or missing/invalid X-Api-Key.

{ "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, or not enough stock.

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

429 Rate limit exceeded. Retry-After is set. Default 60/min; auth, 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 is not configured in /admin.

{ "message": "MojoPay Omni is not configured. …" }

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 Optional Bearer

Create order from cart and initiate Mojo Omni payment. Body includes shipping_address (Ghana region required), optional customer_notes, accept_terms, and optional idempotency_key. Attaches user_id when signed in. Returns 201 with order + payment.checkout_url. Replays of the same idempotency_key return 200 with the original order and checkout URL.

Orders

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

Guest order lookup. Query: email (must match checkout email). Includes fulfillment status, tracking number, handover code, and tracking URL when a shipment exists. Mismatch returns 404. 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. API client must allow external payment. Ghana shipping_address.region is required. Optional idempotency_key replays the original paid order as 200.

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). Same order/fulfillment shape 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" },
    "fulfillment": {
      "number": "FF-ABCDEF1234",
      "status": "packed",
      "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 payload shape; 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" },
    "fulfillment": {
      "number": "FF-ABCDEF1234",
      "status": "shipped",
      "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 payload shape.

{
  "event": "fulfillment.delivered",
  "occurred_at": "2026-08-20T20:00:00+00:00",
  "data": {
    "order": { "number": "MS-ABCDEF1234", "status": "fulfilled" },
    "fulfillment": {
      "number": "FF-ABCDEF1234",
      "status": "delivered",
      "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"
      }
    }
  }
}