Developers

Build on BuyAsATeam

One base URL, two ways to authenticate, and every amount an integer. The guide below covers what a generated reference cannot tell you.

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.

Base URL
https://buyasateam.com/api

Nothing 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.

The first call worth making
curl https://buyasateam.com/api/catalog/deals?per_page=3

Two 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.

Sign in
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 inMeansExample
_minorAn integer amount in the minor unit22200000 is $222,000.00
_bpsA rate in basis points2000 is 20%
_atAn ISO 8601 timestamp in UTC2026-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.

StatusWhen
400The request is malformed or a value is not accepted
401No credentials, or they are wrong or expired
403Authenticated, but not allowed to do this
404No such thing, or nothing you are allowed to see
409The request conflicts with the current state
422The body did not validate; details lists the fields
429Too 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
Read the catalogue from a browser
// 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.

Create or fetch your link for a 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"
  }'
Report a sale
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.

EventFires when
order.createdYou reported a sale. It is already paid and in escrow.
order.deliveredThe supplier marked it shipped. The delivery clock starts here.
order.confirmedDelivery was confirmed — by your customer through you, or automatically once the platform's waiting period has passed. Escrow releases to the supplier.
order.disputedYour customer raised a dispute. Escrow stops until somebody resolves it.
order.shipment_createdA parcel was recorded against an order, with its carrier and tracking number.
order.shipment_updatedA parcel moved: in transit, out for delivery, delivered, or failed.
order.return_openedA buyer asked to send goods back, with the reason and the units.
order.return_decidedThe supplier accepted or refused a return.
order.return_refundedA return was paid back. The amount is the units returned, not the order.
deal.publishedA listing of yours went on sale. Suppliers only — carries your own price, not what a buyer pays.
deal.changes_requestedA listing of yours was refused and came back for correction. Carries the findings, so you can act on them without opening the site.
commission.settledYour shares of an order were booked. Carries only your own lines, each with the external_ref of the person who earned it.
disbursement.paidA 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.

HeaderContains
X-BuyAsATeam-EventThe event name
X-BuyAsATeam-TimestampUnix seconds, and part of the signature
X-BuyAsATeam-SignatureHMAC-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.

Verify a 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."
  }'
CallWho
POST /returns/{id}/decideThe supplier accepts or refuses, with a reason
POST /returns/{id}/receivedThe supplier has the goods back
POST /returns/{id}/refundStaff 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.