401 Missing or invalid Bearer token, or missing/invalid X-Api-Key.
{ "message": "Unauthenticated." }
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.
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.
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. …" }
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.
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.
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.
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.
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).
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.
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"
}
}
}
}