# Build your MeshHub merchant API with an AI coding agent

You are a business with your own booking system, and you want MeshHub to
sell your services. That means exposing six HTTP endpoints that MeshHub
calls. This page is a prompt you can paste into an AI coding agent
(Claude Code, Cursor, Codex, Replit Agent — anything that can fetch URLs
and write code in your stack) to get most of that written for you.

Read this first, so you know what you are agreeing to:

- **You build a server; you do not install a library of ours.** There is
  no merchant SDK, because your booking system is yours — only the wire
  contract is fixed.
- **You issue the API key.** You authenticate MeshHub, not the other way
  around.
- **Availability is computed live, by you, on every search.** There is no
  inventory sync to keep in step, and no cache of yours for us to go
  stale against.
- **An AI agent will get the easy 80% right and the idempotency wrong.**
  Run the conformance suite — the last step in the prompt — before you
  tell us you are ready. It is the same suite MeshHub runs against your
  staging before switching you on.

Swap the first line for your own business and stack, then **paste Block
A followed by the one addendum below that matches your business**. The
core is the same for every kind of merchant; only the shape of an offer
and how it is booked differ (specification §9).

---

## Block A — the core prompt

```text
Add a "MeshHub Merchant Supplier API" to our existing hair-salon booking
system (Django + Postgres). MeshHub is a marketplace that will call this
API to sell our appointments. We implement the server; they call it.

Fetch these first and follow them — the specification is normative and
wins over anything you infer:
https://merchant.meshhub.ai/docs/merchant-api-spec.md
https://merchant.meshhub.ai/docs/merchant-api-openapi.json

Implement these endpoints under /api/supplier/v1/:
  GET  ping/
  GET  search/
  POST price/                            (optional but recommended)
  POST book/
  GET  booking/{supplier_order_id}/
  POST booking/{supplier_order_id}/cancel/
  GET  partners/                         (optional; only if we front
                                          more than one salon, branch
                                          or location)

Authenticate every one of them with an X-Api-Key header against a key we
generate and store hashed. A missing or wrong key is HTTP 401 with the
AUTH_FAILED error envelope — including on ping, which is what MeshHub
uses to prove the whole auth path works.

Get these right; they are where implementations usually fail:

1. An offer is a BOOKABLE THING, not a time. What that thing is, and
   whether it carries a slots[] array, is set by our family addendum
   below — read it before writing the search endpoint. Whatever the
   family, an offer that cannot actually be booked is not returned at
   all rather than returned as an empty one.

2. offer_id is opaque and SIGNED. Encode whatever you need to rebuild
   the thing being offered and sign it with HMAC. Never trust the copy
   that comes back — a tampered or stale token is OFFER_EXPIRED. It
   does NOT encode the booking selector; that rides separately on
   price/ and book/ (see the addendum).

3. Booking is IDEMPOTENT, keyed on the offer's CONTENTS plus whatever
   our family uses to select one booking inside the offer (the
   addendum names it) — never on the signed token, which may differ
   per issuance. If an order with that key exists, return it with HTTP
   200 instead of creating a second one; first creation is HTTP 201.
   Enforce it with a unique constraint and catch the race, do not just
   check-then-insert. Two bookings of the same offer with DIFFERENT
   selectors are two orders, not a replay. Cancel is idempotent too:
   cancelling an already-cancelled order is HTTP 200 with its current
   state, not an error.

4. Timestamps are ISO-8601 WITH a UTC offset, everywhere, in both
   directions. Naive local datetimes are non-compliant and will be
   rejected. If our models store naive local time, convert at the API
   boundary — do not just serialize what is in the column.

5. Money is a decimal STRING ("25.00"), never a float. Price MUST be
   greater than zero: an offer we cannot state a price for must not be
   returned at all. Watch for a price column defaulting to 0 — on the
   wire that is indistinguishable from "free" and the offer gets
   dropped.

6. Honour the mandatory search filters OUR FAMILY declares (the
   addendum lists them), do not just accept and ignore them. The
   category parameter is mandatory for everyone. Ignore any parameter
   you do not recognise rather than erroring — that is what keeps
   another family's parameters harmless.

7. Errors use exactly this envelope: {"code", "message", "detail"},
   with code from AUTH_FAILED, VALIDATION, NOT_FOUND, OFFER_EXPIRED,
   SLOT_TAKEN, RATE_LIMITED, ERROR. Do not invent codes. For
   VALIDATION, put the per-field reason in detail — MeshHub shows that
   to the customer so they can correct it, and a bare "invalid input"
   ends the sale.

8. Order status uses exactly: pending, confirmed, paid, cancelled,
   failed, unknown. Map our internal statuses onto these. Status must
   never move backwards on repeated polls.

9. SCOPE CHECK, this is a security requirement: only orders created
   through this API may be readable or cancellable here. An order id
   from our website, app or front desk must return NOT_FOUND. Our ids
   are sequential, so without this MeshHub could enumerate our whole
   booking table.

10. Allow at least 120 requests/minute. Answer search within 15s and
    book within 30s. Paginate over services and compute slots per page
    so one request can never trigger unbounded work.

Write tests for the idempotency rules, the scope check, and the offset
handling specifically — those are the three that pass a code review and
fail in production.

Finally, fetch and run the conformance suite against our staging until
it is green, and fix what it reports:
https://merchant.meshhub.ai/docs/merchant_api_conformance.py

  python merchant_api_conformance.py \
      --base-url https://staging.our.host \
      --api-key <the key we generated> \
      --category <each slug we declared>

It creates and cancels real reservations, so point it at staging, never
production.
```


## Block B — pick the one addendum for your business

Paste this straight after Block A. Each corresponds to one **family** in
§9 of the specification; if you sell more than one kind of thing, paste
each addendum that applies and expect the conformance suite to check
each separately.

### B1 — Salons, clinics, studios and other appointment businesses

```text
Our offers are APPOINTMENTS (spec §9.1). Category slugs: hair_salon,
massage_center, health_clinic, beauty_salon, spa, fitness,
dental_clinic, pet_service, nail_salon, tennis_court, child_care,
training, housekeeping, pet_care, nursing, refurbishment, restaurant,
car_repair.

One offer = one service, by one staff member, at one place, carrying
EVERY free time it can be booked at in a slots[] array. Do not emit one
offer per time slot. An offer with no free time left is not returned.

Each offer carries master, service and partner blocks — the buyer's
card is built from them, so fill in what we have.

Mandatory search filters: date, date_start/date_end, master_id, and
partner_id if we front more than one salon or branch. With no date
parameter, return a bounded window starting today, at most 7 days.

price/ and book/ carry appointment_at — one of the offer's
slots[].time_start values, verbatim, with its UTC offset. That is the
idempotency selector: meshhub:{service_id}:{staff_id}:{appointment_at}.
```

### B2 — Car rental

```text
Our offers are VEHICLES (spec §9.4). Category slug: car_rental.

One offer = one vehicle or vehicle group we will rent, at a DAILY rate.
There are no slots — the rental window is agreed at book time.

Each offer carries a vehicle block (id and name required; class, seats,
doors, bags, transmission, unlimited_mileage, insurance_type, image_url
and locations optional) and MUST set price_unit to "per_day".

Mandatory search filters: city and pickup_point. Both must be satisfied
by ONE AND THE SAME location row — a car whose only branch in a city is
the airport must not come back for someone asking about the centre. Do
not implement date, date_start/date_end, master_id or partner_id; we
are never sent them.

book/ carries extra.date_from and extra.date_to as ISO calendar dates
(YYYY-MM-DD). date_to before date_from is VALIDATION; a window the
vehicle is not free for is SLOT_TAKEN. Idempotency selector:
meshhub:{vehicle_id}:{date_from}:{date_to}. The order's price is the
TOTAL for the window, not the daily rate.
```

### B3 — Hotels and other accommodation

```text
Our offers are ROOM-NIGHTS (spec §9.5). Category slug: hotel.

One offer = one room, on one rate, for the stay that was searched.
There are no slots.

Each offer carries hotel and room blocks (required) plus rate and stay
(recommended). A stay is [check_in, check_out) — the departure day is
not a night. There is no offer-details endpoint in this spec, so put
the description and image_url a buyer's card needs on the hotel block
itself.

Mandatory search filters: check_in and check_out. A search with no
dates cannot be priced — answer it with VALIDATION rather than guessing
a stay. We may also receive destination, adults, children, rooms,
child_ages, and partner_id carrying a comma-separated list of our hotel
codes.

book/ carries extra.guests, extra.holder and an optional extra.remark.
Idempotency is on the offer alone: it already fixes the stay.
```

### B4 — Flights

```text
Our offers are ITINERARIES (spec §9.6). Category slug: travel.

One offer = one priced itinerary for the passengers searched, bounded
by expires_at. There are no slots.

Each offer carries a flight block: airline, origin_airport and
destination_airport as three-letter IATA codes, legs as readable
segment strings, departure_time and arrival_time as ISO-8601 WITH a UTC
offset, duration, price_per_person, and optionally fare_code,
fare_brand and fare_includes. Omit the fare_* keys entirely rather than
sending null.

Mandatory search filters: origin, destination and date_start. A search
with no route or no date cannot be priced — answer it with VALIDATION.
We may also send date_end, trip_type, cabin, airlines, flexibility,
adults/children/infants and passenger_ages.

book/ carries extra.passengers (firstName, lastName, gender,
dateOfBirth) and an optional extra.contact. Idempotency is on the offer
alone.

IMPORTANT — what this spec does NOT cover: there are no ancillaries.
No seat maps, no baggage add-ons, no meals, no lounge access, and no
payment step. Price everything into the fare or list it in
fare_includes. Do not build endpoints for extras; MeshHub will not call
them.
```

### B5 — Parcel delivery and couriers

```text
Our offers are QUOTES (spec §9.7). Category slug: shipping.

One offer = one carrier and service, priced for the parcel described in
the search. There are no slots.

Each offer carries a shipment block: origin and destination as
ISO-3166-1 alpha-2 country codes, weight_kg as a decimal string above
zero, plus transportation_type, package_type, carrier, service_name,
transit_days and dimensions. Echo the route and weight we actually
priced — a buyer comparing two quotes needs to know they are for the
same parcel.

Mandatory search filters: origin, destination and weight_kg. A quote
without them is not a quote — answer with VALIDATION. We may also
receive dim_w/dim_h/dim_l, package_type and transportation_type.

book/ carries extra.shipper_details and extra.consignee_details (each
contact_name, phone, email_address, address, city, postcode, country)
plus extra.parcels. A package_type or transportation_type contradicting
the quoted offer is VALIDATION. Idempotency is on the offer alone.

IMPORTANT — what this spec does NOT cover: there is no label endpoint
and no tracking feed. If we have a tracking number or a label URL, put
them on the order body; MeshHub polls booking/{id}/ and surfaces what
is there. Note also that settlement is deferred, so book/ is what
engages the courier — there is no later payment step that confirms it.
```

---

## After it is green

Send MeshHub the go-live checklist from §10 of the specification: your
declared category slugs, your staging and production base URLs and keys,
an optional OpenAPI URL for your own implementation, and a technical
contact for incidents. If you signed up through the supplier portal, the
API integration screen collects the same things and runs the checks for
you.
