Skip to content

API reference

Customer accounts

Logins for the shop's customers, their profile, addresses, points, wishlist and reviews. Registering or logging in gives the website a token: two parts joined by a bar, such as 12|dpc_x8Qm..., the second starting dpc_. Keep it and send it with each call that needs a login as Authorization: Bearer <token>, along with the storefront key, which finds the shop the token belongs to. A token works for 90 days from the last call that used it; after that, or once it is logged out or the password changes, a call answers 401 with the message Unauthenticated. The customer is the same record the till and back office see. Password reset emails link to the website address the shop sets on its Storefront API page, at /reset-password on that address.

Every call sends the shop's key in the X-Storefront-Key header and asks for JSON. Calls marked with a customer token also send the signed-in customer's token. The integration guide explains keys, answers, errors and limits.

Signing in

POST

/api/v1/auth/register

Scope
accounts
Customer token
none
Limit
5 a minute

A login for the shop's customer, logged in at once: answers 201 with the customer and a token. When the shop already has a customer without a login whose phone and email both match (the phone in any spelling, such as 0712..., +254712... or 712..., and the email in any case), the login joins that record, keeping its name, points and orders; the name sent is then not used. Otherwise it is a new customer from the website, refused while the phone or the email belongs to someone else. A referral code that does not work is ignored and never stops the account.

Parameters

name string · body · required
Up to 255 characters; HTML tags are removed.
email string · body · required
Up to 255 characters, kept in lower case.
phone string · body · required
Up to 20 characters, kept as typed. It must read as a phone number, or registering answers 422 with "Enter a phone number, such as 0712 345 678."
password string · body · required
At least 8 characters.
password_confirmation string · body · required
The same password again.
referral_code string · body
Another customer's referral code, up to 12 characters, in any case.
marketing_consent boolean · body
true records consent to marketing from sign-up; false records a refusal. Leave it out to record neither.
device_name string · body
Names the token, such as Chrome on Windows; cut to 100 characters. Defaults to website.

Each refusal answers 422 with the field in errors. An email that already has a login: "An account with this email already exists. Log in, or reset your password." (email). A phone the shop holds for a customer whose email is different: "Bike Yard already has this phone number with other details. Register with the email Bike Yard has for you, or ask Bike Yard to update your record." (phone). An email the shop holds with a different phone: "Bike Yard already has this email with another phone number. Register with the phone Bike Yard has for you, or ask Bike Yard to update your record." (email). The shop's own name takes the place of Bike Yard. Show these messages as they are: they tell the person what to do.

Example answer
{
  "data": {
    "user": {
      "id": "01a16731-b30f-7197-9f60-6bdb5f10511d",
      "name": "Wanjiru Kamau",
      "email": "wanjiru@example.com",
      "phone": "0711 000 001"
    },
    "token": "12|dpc_x8QmT4vLr2NpK7aZc9WbE3hJ5sGd1YfUo6RkMn0qbfa40aba"
  }
}
POST

/api/v1/auth/login

Scope
accounts
Customer token
none
Limit
10 a minute

Logs a customer in with their email, in any case, and password, and gives a new token. Each login is its own token, so a customer can be logged in on several devices at once.

Parameters

email string · body · required
The email the login was registered with.
password string · body · required
The customer's password.
device_name string · body
Names the token, up to 100 characters. Defaults to website.

A wrong email or password answers 422 with "Invalid email or password." on email, the same for both so it never tells whether an email has a login.

Example answer
{
  "data": {
    "user": {
      "id": "01a16731-b30f-7197-9f60-6bdb5f10511d",
      "name": "Wanjiru Kamau",
      "email": "wanjiru@example.com",
      "phone": "0711 000 001"
    },
    "token": "12|dpc_x8QmT4vLr2NpK7aZc9WbE3hJ5sGd1YfUo6RkMn0qbfa40aba"
  }
}
POST

/api/v1/auth/logout

Scope
accounts
Customer token
required

Deletes the token the call carries, so it stops working. The customer's other tokens, on other devices, keep working.

Example answer
{
  "data": {
    "message": "Logged out"
  }
}
GET

/api/v1/auth/user

Scope
accounts
Customer token
required

The customer the token belongs to. Use it when the website loads to check a kept token still works: a token that no longer works answers 401.

Example answer
{
  "data": {
    "user": {
      "id": "01a16731-b30f-7197-9f60-6bdb5f10511d",
      "name": "Wanjiru Kamau",
      "email": "wanjiru@example.com",
      "phone": "0711 000 001"
    }
  }
}
POST

/api/v1/auth/forgot-password

Scope
accounts
Customer token
none
Limit
5 a minute

Emails the customer a link to choose a new password, if the email has a login. The answer is the same whether it does or not. The link is the website address set on the shop's Storefront API page followed by /reset-password?token=...&email=..., and works once, for 60 minutes. A new link goes at most once a minute for each customer; asking again sooner sends nothing and answers the same.

Parameters

email string · body · required
The email the login was registered with.

Until the shop sets its website address, it answers 422 with "Password resets are not set up for this shop's website yet." on email. The website needs a page at /reset-password that reads token and email from the address and sends them to reset-password with the new password.

Example answer
{
  "data": {
    "message": "If an account exists with that email, a password reset link has been sent."
  }
}
POST

/api/v1/auth/reset-password

Scope
accounts
Customer token
none
Limit
5 a minute

Sets a new password from the emailed link and logs the customer in at once with a new token. Every other token the customer had is deleted, so every other device is logged out, and the link stops working.

Parameters

token string · body · required
The token from the link's address.
email string · body · required
The email from the link's address.
password string · body · required
At least 8 characters.
password_confirmation string · body · required
The same password again.
device_name string · body
Names the new token, up to 100 characters. Defaults to website.

A wrong, used or expired link answers 422 with the message "Unable to reset password. The link may have expired." and "This password reset link is not valid." on email. Offer to send a new link.

Example answer
{
  "data": {
    "message": "Your password has been reset.",
    "user": {
      "id": "01a16731-b30f-7197-9f60-6bdb5f10511d",
      "name": "Wanjiru Kamau",
      "email": "wanjiru@example.com",
      "phone": "0711 000 001"
    },
    "token": "13|dpc_Hd3nW8pQe1LzR6tYc0VbM5kJ2aXs9GfU4oNi7EwTc5add5e3"
  }
}

Profile and password

GET

/api/v1/account/profile

Scope
accounts
Customer token
required

The customer's name, email and phones.

Example answer
{
  "data": {
    "id": "01a16731-b30f-7197-9f60-6bdb5f10511d",
    "name": "Wanjiru Kamau",
    "email": "wanjiru@example.com",
    "phone": "0711 000 001",
    "alternate_phone": "0722 000 002"
  }
}
PATCH

/api/v1/account/profile

Scope
accounts
Customer token
required

Changes the customer's name and phones and answers with the profile as GET account/profile does. Send all three each time: an alternate phone left out is cleared. The email cannot be changed here.

Parameters

name string · body · required
Up to 255 characters; HTML tags are removed.
phone string · body · required
Up to 20 characters, kept as typed.
alternate_phone string · body
Up to 20 characters.
Example answer
{
  "data": {
    "id": "01a16731-b30f-7197-9f60-6bdb5f10511d",
    "name": "Wanjiru Kamau",
    "email": "wanjiru@example.com",
    "phone": "0711 000 001",
    "alternate_phone": "0722 000 002"
  }
}
PATCH

/api/v1/account/password

Scope
accounts
Customer token
required

Changes the password, given the current one. Every other token the customer has is deleted, so other devices are logged out; the token that made the call keeps working.

Parameters

current_password string · body · required
The password the customer has now.
password string · body · required
At least 8 characters.
password_confirmation string · body · required
The same password again.

A wrong current password answers 422 with "The current password is incorrect." on current_password.

Example answer
{
  "data": {
    "message": "Password updated successfully."
  }
}

Addresses

GET

/api/v1/account/addresses

Scope
accounts
Customer token
required

The customer's saved addresses, the default first, then the most recently changed. A customer has at most one default.

Example answer
{
  "data": [
    {
      "id": "01a16732-05e5-7146-b9ab-6b4b82508f9e",
      "label": "Home",
      "name": "Wanjiru Kamau",
      "phone": "0711 000 001",
      "address_line_1": "Kenyatta Avenue, Ngong Lane Plaza",
      "address_line_2": "3rd floor, Suite 12",
      "city": "Nairobi",
      "county": "Nairobi",
      "postal_code": "00100",
      "is_default": true
    }
  ]
}
POST

/api/v1/account/addresses

Scope
accounts
Customer token
required

Saves a new address and answers 201 with it. The customer's first address becomes the default, and so does one sent with is_default true, which takes the default from the others. HTML tags and surrounding spaces are removed from every field.

Parameters

label string · body
A short name such as Home or Office, up to 50 characters.
name string · body · required
Who receives deliveries there, up to 255 characters.
phone string · body · required
Up to 20 characters.
address_line_1 string · body · required
Up to 255 characters.
address_line_2 string · body
Up to 255 characters.
city string · body · required
Up to 100 characters.
county string · body · required
Up to 100 characters.
postal_code string · body
Up to 20 characters.
is_default boolean · body
true makes it the default address.
Example answer
{
  "data": {
    "id": "01a16732-05e5-7146-b9ab-6b4b82508f9e",
    "label": "Home",
    "name": "Wanjiru Kamau",
    "phone": "0711 000 001",
    "address_line_1": "Kenyatta Avenue, Ngong Lane Plaza",
    "address_line_2": "3rd floor, Suite 12",
    "city": "Nairobi",
    "county": "Nairobi",
    "postal_code": "00100",
    "is_default": true
  }
}
PATCH

/api/v1/account/addresses/{id}

Scope
accounts
Customer token
required

Changes one of the customer's addresses and answers with it. It takes the same fields as adding one and replaces them all, so send the whole address, not only what changed; a label left out keeps the one it had. is_default true makes it the default; left out, the address keeps whether it was the default.

Parameters

id string · path · required
The address's id.
label, name, phone, address_line_1, address_line_2, city, county, postal_code, is_default mixed · body
As for adding an address; name, phone, address_line_1, city and county are required.

An id that is not one of this customer's addresses answers 404.

Example answer
{
  "data": {
    "id": "01a16732-062d-70cb-a2cc-335eb5d28850",
    "label": "Office",
    "name": "Wanjiru Kamau",
    "phone": "0711 000 001",
    "address_line_1": "Westlands Road, The Mirage",
    "address_line_2": "Tower 2, 7th floor",
    "city": "Nairobi",
    "county": "Nairobi",
    "postal_code": null,
    "is_default": true
  }
}
DELETE

/api/v1/account/addresses/{id}

Scope
accounts
Customer token
required

Deletes one of the customer's addresses. Deleting the default makes the most recently changed of the rest the default.

Parameters

id string · path · required
The address's id.

An id that is not one of this customer's addresses answers 404.

Example answer
{
  "data": {
    "message": "Address deleted."
  }
}

Loyalty (My Garage)

GET

/api/v1/account/loyalty

Scope
accounts
Customer token
required

Everything the website's My Garage page shows, read from the same points ledger the till uses; nothing here moves points. enabled says whether the shop runs loyalty, and name is what it calls its programme ('{shop} Rewards' until it names one), for the page's title. balance is the points held and value what they are worth off an order, in the shop's currency. tier is null until the customer reaches the first tier; tier spend counts everything spent, or only recent months when spend_basis is rolling. next_tier is null at the top tier, and its progress is a percentage from the tier held to the next. expiring_soon gives the points that expire within 90 days and the first date. entitlements are perks the customer has claimed and not yet used: show the code, which the shop's staff enter at the till. history is the 20 newest ledger rows, newest first; type is earn, redeem, referral, bonus, expire, reversal or opening, and points are negative when they leave. referral is the customer's own code and what it has brought them; a referral pays only after escrow_hours. programme is the shop's terms: cash_off_rate is what one point is worth, a customer earns points_per_unit points for each spend_per_point spent, points can pay up to max_points_share_of_order of an order (0.3 is 30%), and points expire after expiry_months.

tiers lists every tier, lowest first; the example shows two. A customer with no points yet gets balance 0, tier null and empty lists. When enabled is false, hide the page rather than show zeros.

Example answer
{
  "data": {
    "enabled": true,
    "name": "Bike Yard Rewards",
    "balance": 693,
    "value": 1386,
    "tier": "silver",
    "tier_label": "Silver",
    "qualifying_spend": 64300,
    "spend_basis": "lifetime",
    "next_tier": {
      "tier": "gold",
      "label": "Gold",
      "threshold": 75000,
      "remaining": 10700,
      "progress": 78.6
    },
    "tiers": [
      {
        "tier": "silver",
        "label": "Silver",
        "threshold": 25000,
        "reached": true
      },
      {
        "tier": "gold",
        "label": "Gold",
        "threshold": 75000,
        "reached": false
      }
    ],
    "expiring_soon": {
      "points": 0,
      "at": null
    },
    "entitlements": [
      {
        "code": "BY-F9BHXW",
        "label": "Helmet deep-clean & sanitisation",
        "points": 100,
        "issued_at": "2026-10-21"
      }
    ],
    "history": [
      {
        "id": "33a993f8-d2fd-4be8-8fb9-ab617402d1f9",
        "type": "earn",
        "points": 310,
        "spend_basis": 31000,
        "note": null,
        "order_number": "BY-000014",
        "expires_at": "2027-10-21",
        "created_at": "2026-10-21T07:33:00+00:00"
      },
      {
        "id": "2956f601-8885-42e4-a6ee-30aca78c42dc",
        "type": "redeem",
        "points": -100,
        "spend_basis": 0,
        "note": "Helmet deep-clean & sanitisation",
        "order_number": null,
        "expires_at": null,
        "created_at": "2026-10-21T13:10:00+00:00"
      }
    ],
    "referral": {
      "enabled": true,
      "code": "BYP7YQJY",
      "minimum_spend": 7500,
      "reward_points": 150,
      "escrow_hours": 48,
      "referred_count": 2,
      "posted_count": 1,
      "points_earned": 150
    },
    "programme": {
      "cash_off_rate": 2,
      "spend_per_point": 100,
      "points_per_unit": 1,
      "max_points_share_of_order": 0.3,
      "expiry_months": 12
    }
  }
}

Wishlist

GET

/api/v1/wishlist

Scope
accounts
Customer token
required

The customer's saved products that the website still shows, as product cards with the price and stock of the branch the website sells from, the same shape as the product list. Newest products first, by when the product was added to the shop, not when it was saved. The whole list comes at once, with no pages.

A saved product the website stops showing drops out of the list, and comes back if the shop shows it again.

Example answer
{
  "data": [
    {
      "_id": "01a16356-8703-7189-a5be-b8fad6fc78bf",
      "name": "AGV K1 S helmet",
      "slug": "agv-k1-s-helmet",
      "sku": "AGV-K1S-M",
      "description": null,
      "short_description": null,
      "price": 28500,
      "sale_price": null,
      "discount_percentage": null,
      "cost_price": null,
      "stock_quantity": 100,
      "manage_stock": true,
      "in_stock": true,
      "is_featured": false,
      "is_new": true,
      "has_variants": true,
      "is_active": true,
      "weight": null,
      "weight_unit": null,
      "images": [],
      "primary_image": null,
      "category": {
        "_id": "01a16356-8690-7032-8482-2c7bc9a66b63",
        "id": "01a16356-8690-7032-8482-2c7bc9a66b63",
        "name": "Helmets",
        "slug": "helmets",
        "description": null,
        "image_url": null,
        "parent_id": null
      },
      "parent_category": null,
      "brand": {
        "_id": "01a16356-86fd-72ae-aab2-1eef2196e387",
        "id": "01a16356-86fd-72ae-aab2-1eef2196e387",
        "name": "AGV",
        "slug": "agv",
        "logo_url": null,
        "description": null
      },
      "attributes": [],
      "tags": [],
      "meta_title": null,
      "meta_description": null,
      "created_at": "2026-10-22T11:19:37.000000Z",
      "updated_at": "2026-10-21T07:30:00.000000Z"
    }
  ]
}
POST

/api/v1/wishlist/{productId}

Scope
accounts
Customer token
required

Saves a product to the wishlist, or takes it off when it is saved already. added says which happened, and wishlist_count is how many products the customer now has saved, for the heart badge.

Parameters

productId string · path · required
The product's _id from the product list.

A product the website does not show answers 404. wishlist_count counts every saved product, including any the website no longer shows, so it can be more than the list holds.

Example answer
{
  "data": {
    "added": true,
    "wishlist_count": 2
  }
}

Reviews

POST

/api/v1/products/{slug}/reviews

Scope
accounts
Customer token
required
Limit
10 a minute

The customer rates a product the website shows, with a comment and up to three photos. Answers 201 with the review's id; the review waits for the shop's staff to approve it before it shows or counts in the product's rating. A customer has one review for each product: writing again changes it, keeps the photos it had and adds any new ones, and sends it back for approval, taking an approved review off the product until it is approved again. Send it as multipart/form-data when it has photos.

Parameters

slug string · path · required
The product's slug.
rating integer · body · required
1 to 5.
comment string · body · required
Up to 2,000 characters.
photos[] file · body
JPEG, PNG or WebP images, up to 5 MB each. A review holds three at most, counting the photos it already has.

A product the website does not show answers 404. More photos than the review has room for answers 422 with "A review holds up to 3 photos." on photos. One photo may also be sent alone, as photos. Approved reviews are read with GET products/{slug}/reviews.

Example answer
{
  "data": {
    "id": "01a16733-09de-7273-89e5-43c453ae8376",
    "message": "Review submitted and awaiting moderation."
  }
}