Wasanidi
Jenga juu ya BuyAsATeam
URL moja ya msingi, njia mbili za uthibitishaji, na kila kiasi ni namba kamili. Mwongozo ulio hapa chini unaeleza yale ambayo marejeleo yaliyotengenezwa kiotomatiki hayawezi kukueleza.
Getting started
Everything is served from one origin under /api. The services behind it are not addressable from outside, so there is one base URL and one set of rules.
https://buyasateam.com/apiNothing here needs an account to read. The catalogue, a price quote and the margin breakdown are all public, so you can try the API before you have a key.
curl https://buyasateam.com/api/catalog/deals?per_page=3Two things need one: acting as a person, and acting as a machine. They authenticate differently and are covered next.
Authenticating
A bearer token is for acting as a person — their cart, their orders, their commissions. Sign in and you get an access token and a refresh token.
curl -X POST https://buyasateam.com/api/auth/login \
-H 'content-type: application/json' \
-d '{"email":"you@example.com","password":"…"}'{
"access_token": "eyJhbGciOi…",
"refresh_token": "eyJhbGciOi…",
"token_type": "bearer"
}Send the access token on every call after that.
-H 'authorization: Bearer eyJhbGciOi…'It expires. Exchange the refresh token at POST /auth/refresh for a new pair; the old refresh token dies with the exchange, so a stolen one stops working the moment the real owner refreshes.
An API key is for a machine acting on its own behalf — a host platform reporting sales, a script pulling commissions. Create one under Dashboard → Developer. It is shown once.
-H 'X-API-Key: ipo_live_<prefix>_<secret>'Conventions
Every amount is an integer in the currency's minor unit. $222,000.00 is 22200000. There are no floating point amounts anywhere in this API: a hundredth of a cent lost per order is a discrepancy nobody can reconcile a year later.
Rates are in basis points. 20% is 2000; 5% is 500.
| Field ends in | Means | Example |
|---|---|---|
| _minor | An integer amount in the minor unit | 22200000 is $222,000.00 |
| _bps | A rate in basis points | 2000 is 20% |
| _at | An ISO 8601 timestamp in UTC | 2026-08-12T09:15:00Z |
Lists are paged with page and per_page, and answer with the total.
{ "items": [ … ], "total": 143, "page": 1, "per_page": 20 }Errors
A failure is always the same shape, so one handler covers all of them.
{
"error": {
"code": "email_taken",
"message": "An account already exists for this email"
}
}Match on code. The message is written for a person to read and may be reworded without notice; the code is the contract. Some errors carry a details object naming the fields at fault.
| Status | When |
|---|---|
| 400 | The request is malformed or a value is not accepted |
| 401 | No credentials, or they are wrong or expired |
| 403 | Authenticated, but not allowed to do this |
| 404 | No such thing, or nothing you are allowed to see |
| 409 | The request conflicts with the current state |
| 422 | The body did not validate; details lists the fields |
| 429 | Too many requests this minute |
Reading the catalogue
Deals, categories and a single deal with its full margin breakdown.
curl 'https://buyasateam.com/api/catalog/deals?category=medical-equipment&per_page=5'
curl https://buyasateam.com/api/catalog/categories
curl https://buyasateam.com/api/catalog/deals/advanced-mri-system// Public, so no key and no CORS trouble: this runs from a page.
const res = await fetch(
"https://buyasateam.com/api/catalog/deals?per_page=6",
);
const { items } = await res.json();
for (const deal of items) {
// Amounts are integers in minor units — divide only to display.
const price = (deal.buyer_price_minor / 100).toLocaleString(undefined, {
style: "currency",
currency: deal.currency,
});
console.log(deal.title, price);
}A deal carries both prices. supplier_price_minor is what the supplier receives; buyer_price_minor is what a buyer pays, the supplier price plus the 20% markup. margin_breakdown says where that markup goes, to the cent.
Pricing a purchase
Ask before committing. The quote applies the referral discount when one is due, and it is the same code path the order uses, so what it says is what will be charged.
curl -X POST https://buyasateam.com/api/checkout/quote \
-H 'content-type: application/json' \
-d '{"deal":"advanced-mri-system","quantity":3,"referral_code":"3XHWCBD2"}'Referral links
A member shares a deal and earns 20% of the markup when somebody buys through it. The link is /r/CODE and resolves to the deal.
curl -X POST https://buyasateam.com/api/share-links \
-H 'authorization: Bearer <token>' \
-H 'content-type: application/json' \
-d '{"deal":"advanced-mri-system"}'{ "code": "3XHWCBD2", "url_path": "/r/3XHWCBD2", "click_count": 0 }Running BuyAsATeam behind your own platform
Your customers buy on your site. You report the sale, we book the escrow and split the markup, and your users see their earnings here. You keep the customer relationship; we keep the ledger.
curl -X POST https://buyasateam.com/api/orders/external -H 'X-API-Key: ipo_live_…' -H 'content-type: application/json' -d '{
"deal": "advanced-mri-system",
"quantity": 1,
"external_ref": "your-order-1042",
"external_customer_ref": "your-customer-77"
}'const res = await fetch("https://buyasateam.com/api/orders/external", {
method: "POST",
headers: {
"content-type": "application/json",
"X-API-Key": process.env.BAT_API_KEY,
},
body: JSON.stringify({
deal: "advanced-mri-system",
quantity: 1,
external_ref: "your-order-1042",
external_customer_ref: "your-customer-77",
}),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${error.code}: ${error.message}`);
}
const order = await res.json();Your end customer has no account here, so your platform is the buyer of record and their id travels as external_customer_ref. Nothing is paid out at this point: the whole amount sits in escrow until delivery is confirmed.
The shares nobody on this platform claimed — the referral link's 20% and the direct inviter's 5% — are yours. Dispatch them to whoever earned them on your side however you like; we book them, hold them in your balance, and itemise them for you.
Letting your own users share
One of your users wants to share a product and earn on it. They have no account here, so you create the link on their behalf and name them with your own id.
curl -X POST https://buyasateam.com/api/share-links -H 'X-API-Key: ipo_live_…' -H 'content-type: application/json' -d '{"deal_slug":"advanced-mri-system","external_owner_ref":"your-user-42"}'{
"code": "4H6QRV8J",
"url_path": "/r/4H6QRV8J",
"external_owner_ref": "your-user-42",
"click_count": 0
}Same user, same deal, same code: ask again and you get the link you already had, with its counters intact. Pass the code as referral_code when you report the sale and the 20% is booked against that reference.
Reconciling what you are owed
Every line, plus a total per person. This is what turns one balance into a payout run.
curl -H 'X-API-Key: ipo_live_…' 'https://buyasateam.com/api/commissions/statement?since=2026-08-01'{
"currency": "USD",
"balance_minor": 625000,
"owed_by_external_ref": [
{ "external_ref": "your-user-42", "amount_minor": 500000 },
{ "external_ref": "", "amount_minor": 125000 }
],
"lines": [
{ "role": "link_owner", "external_ref": "your-user-42", "amount_minor": 500000 }
]
}A line with an empty external_ref is your platform's own earning. Everything else is money you are holding for one of your users.
Webhooks
Register an https endpoint and we tell you when something happens, instead of you polling for it.
| Event | Fires when |
|---|---|
| order.created | You reported a sale. It is already paid and in escrow. |
| order.delivered | The supplier marked it shipped. The delivery clock starts here. |
| order.confirmed | Delivery was confirmed — by your customer through you, or automatically once the platform's waiting period has passed. Escrow releases to the supplier. |
| order.disputed | Your customer raised a dispute. Escrow stops until somebody resolves it. |
| order.shipment_created | A parcel was recorded against an order, with its carrier and tracking number. |
| order.shipment_updated | A parcel moved: in transit, out for delivery, delivered, or failed. |
| order.return_opened | A buyer asked to send goods back, with the reason and the units. |
| order.return_decided | The supplier accepted or refused a return. |
| order.return_refunded | A return was paid back. The amount is the units returned, not the order. |
| deal.published | A listing of yours went on sale. Suppliers only — carries your own price, not what a buyer pays. |
| deal.changes_requested | A listing of yours was refused and came back for correction. Carries the findings, so you can act on them without opening the site. |
| commission.settled | Your shares of an order were booked. Carries only your own lines, each with the external_ref of the person who earned it. |
| disbursement.paid | A withdrawal of yours was paid out. Carries the same owed_by_external_ref breakdown as the statement, so you can dispatch it. |
curl -X POST https://buyasateam.com/api/developer/webhooks -H 'X-API-Key: ipo_live_…' -H 'content-type: application/json' -d '{"url":"https://your-site.example/hooks/bat","events":["order.confirmed"]}'The response carries the signing secret, once. Every delivery is signed with it.
| Header | Contains |
|---|---|
| X-BuyAsATeam-Event | The event name |
| X-BuyAsATeam-Timestamp | Unix seconds, and part of the signature |
| X-BuyAsATeam-Signature | HMAC-SHA256 of `timestamp.body`, hex |
Verify it over the raw body, before parsing. Re-serialising JSON reorders keys and changes the digest, which would reject every genuine delivery.
import crypto from "node:crypto";
import express from "express";
const app = express();
// The raw body, not a parsed and re-serialised one: JSON.stringify reorders
// keys and changes the digest, which rejects every genuine delivery.
app.post("/hooks/bat", express.raw({ type: "*/*" }), (req, res) => {
const timestamp = req.get("X-BuyAsATeam-Timestamp");
const signature = req.get("X-BuyAsATeam-Signature");
const expected = crypto
.createHmac("sha256", process.env.BAT_WEBHOOK_SECRET)
.update(timestamp + "." + req.body)
.digest("hex");
const ok =
signature &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
// Older than five minutes is a replay of something we already handled.
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
if (!ok || !fresh) return res.sendStatus(400);
const { event, data } = JSON.parse(req.body);
if (event === "order.confirmed") payOut(data.external_ref);
// 2xx and we stop retrying. 4xx and we stop too.
res.sendStatus(200);
});Confirming delivery from your side is an ordinary call — your key is the buyer of record on your own external orders, so it is allowed:
curl -X POST https://buyasateam.com/api/orders/<order_id>/confirm -H 'X-API-Key: ipo_live_…'That is the moment escrow releases, the supplier is paid and all seven shares settle.
Nominating a BME
A BME visits a supplier, inspects the goods and signs off before a deal is listed. They earn 5% of the margin on every sale of every deal they verify, for the life of that deal.
If you have people on the ground, put them forward:
curl -X POST https://buyasateam.com/api/bme/applications -H 'X-API-Key: ipo_live_…' -H 'content-type: application/json' -d '{
"email": "inspector@your-site.example",
"full_name": "Amina Diallo",
"country": "SN",
"city": "Dakar",
"experience": "Six years inspecting medical equipment imports.",
"external_ref": "your-staff-8"
}'Questions on a listing
A buyer with a question about a listing does not have to buy it first. Questions sit on the deal, the supplier answers, and the answer is public — so the next buyer reads it rather than asking again.
curl -X POST https://buyasateam.com/api/inquiries -H 'Authorization: Bearer <token>' -H 'content-type: application/json' -d '{
"deal_slug": "advanced-mri-system",
"question": "Does it ship with the 400V transformer, or is that separate?",
"is_public": true
}'GET /inquiries?deal_slug=… returns the public thread for a listing, with answers. GET /inquiries/mine returns yours, private ones included.
Parcels and returns
An order used to go from paid to shipped and say nothing more until the buyer confirmed. Parcels carry their own history, and one order may have several: fifty units in three crates is three tracking numbers and three arrival dates.
curl -X POST https://buyasateam.com/api/orders/ORD-7K2M4Q/shipments -H 'X-API-Key: ipo_live_…' -H 'content-type: application/json' -d '{
"carrier_name": "Maersk",
"tracking_number": "MAEU7712345",
"tracking_url": "https://www.maersk.com/tracking/MAEU7712345",
"expected_at": "2026-09-18T00:00:00Z",
"quantity": 20
}'Recording a parcel also moves the order to shipped, so a supplier does not press a second button meaning the same thing. Move it along with POST /orders/shipments/{id}/events, whose status is one of preparing, in_transit, out_for_delivery, delivered or failed.
A buyer who received damaged, wrong or short goods opens a return against a confirmed order. The refund is computed from the order pro rata to the units coming back — a buyer naming their own figure is a negotiation, not a claim.
curl -X POST https://buyasateam.com/api/returns -H 'Authorization: Bearer <token>' -H 'content-type: application/json' -d '{
"order_id": "6f1c…",
"reason": "damaged",
"quantity": 2,
"detail": "Two crates arrived with the housing cracked."
}'| Call | Who |
|---|---|
| POST /returns/{id}/decide | The supplier accepts or refuses, with a reason |
| POST /returns/{id}/received | The supplier has the goods back |
| POST /returns/{id}/refund | Staff pay the buyer back, through the escrow ledger |
Buying together
Two shapes, and they are different purchases. A unit group gathers buyers until a supplier's minimum order is met, and each owns the units they paid for. A value group gathers money for one indivisible thing, owned pro rata afterwards and named on the receipt.
curl -X POST https://buyasateam.com/api/checkout/groups -H 'Authorization: Bearer <token>' -H 'content-type: application/json' -d '{ "deal": "advanced-mri-system", "mode": "value", "window_days": 30 }'POST /checkout/groups/{reference}/join takes units for a unit group and amount_minor for a value one. Each contribution is a whole order: its own referral attribution, its own escrow line, its own commissions, its own confirmation.
A group that does not fill before its window closes refunds every contribution in full. Nothing is kept for trying.
Standing orders
For goods bought on a rhythm. A standing order places the paperwork on schedule and never takes money: each due date creates an order awaiting payment, exactly as if the buyer had opened the listing and pressed buy.
curl -X POST https://buyasateam.com/api/subscriptions -H 'Authorization: Bearer <token>' -H 'content-type: application/json' -d '{
"deal": "commercial-bakery-equipment-line",
"quantity": 2,
"interval": "monthly",
"payment_method": "bank_transfer",
"max_deliveries": 12
}'Interval is weekly, fortnightly, monthly or quarterly. PATCH /subscriptions/{id} pauses, resumes or stops one; GET /subscriptions/{id}/deliveries lists what it has placed.
Sale prices and promotion codes
Two ways a purchase costs less, and which one is in play decides who pays for it.
- A sale window belongs to the supplier: a lower price for a period, which the platform's markup follows down. Everybody buying in the window gets it, no code needed.
- A promotion code belongs to the platform, and comes out of the platform's share of the markup — never out of commission owed to the link owner, the inviter, the recommender or the BME.
curl -X POST https://buyasateam.com/api/coupons/check -H 'Authorization: Bearer <token>' -H 'content-type: application/json' -d '{
"code": "HARVEST26",
"deal_slug": "precision-agriculture-drone-fleet",
"total_minor": 4500000,
"margin_minor": 750000
}'The bell
In-app notifications addressed to one member: a parcel delivered, a return accepted, a listing sent back for correction, a bid on their request.
curl https://buyasateam.com/api/notifications?unread_only=true -H 'Authorization: Bearer <token>'Each carries a kind the client translates, params to interpolate, and an href which is always a path on this site. POST /notifications/{id}/read clears one and POST /notifications/read-all clears the lot.
Rate limits
Requests are counted per caller per minute. Over the limit answers 429 with code rate_limited. Back off and retry; the window is a rolling minute, not a fixed one, so waiting the shortest sensible time works.