@meshhub/sdk
v0.3.0
Published
Official TypeScript SDK for the MeshHub tenant API — search, book, pay, and track orders across travel, shipping, car-rental, hotel, and salon suppliers.
Maintainers
Readme
@meshhub/sdk
Official TypeScript SDK for the MeshHub tenant API — search, book, pay, and track orders across travel, shipping, car-rental, hotel, and salon suppliers from one typed client.
Zero runtime dependencies. Node ≥ 18 (uses global fetch). Ships ESM + CJS
with full type declarations generated from the live OpenAPI contract.
Install
npm install @meshhub/sdkSetup
import { MeshHubClient } from "@meshhub/sdk";
const meshhub = new MeshHubClient({
apiKey: process.env.MESHHUB_API_KEY!, // "mh_..." — keep it server-side only
baseUrl: "https://merchant.meshhub.ai",
});
await meshhub.health(); // { status: "ok" }Never expose the API key in browser code. Call MeshHub from your server (API route, backend) and keep the key in an environment secret.
Search → book → pay
// 1. Search offers (async task handled for you)
const { offers, field_schema } = await meshhub.searchAndWait({
category: "shipping",
extra: { origin: "DE", destination: "ES", weight_kg: 2.5 }, // ISO 3166-1 alpha-2
});
const offer = offers[0];
// 2. Book the offer. Pass your own customer id so MeshHub stores the
// customer, prefills repeat bookings, and can charge saved cards later.
const order = await meshhub.bookAndWait({
category: "shipping",
supplier_id: offer.supplier_id,
offer_id: offer.offer_id,
end_user_external_id: "user-42",
end_user: {
first_name: "Ada",
last_name: "Lovelace",
email: "[email protected]",
phone: "+441234567",
},
extra: { /* per-category fields — collect what field_schema marks required */ },
quoted_price: offer.price,
});
// 3. Take payment. Branch on requires_action:
const payment = await meshhub.payments.payOrder(order.supplier_order_id);
if (payment.requires_action) {
// First payment for this customer: open the Revolut checkout with
// payment.order_token (JS widget) or redirect to payment.checkout_url.
} else {
// Saved card charged server-side — nothing to render.
}
// The definitive outcome arrives via the payment_succeeded /
// payment_failed webhook (see below).searchAndWait / bookAndWait dispatch the operation and poll
GET /tasks/{id}/ until it finishes — immediately, then backing off
1 s → 2 s → 4 s → 8 s (cap) up to a 120 s budget, so fast searches
resolve in a poll or two and long tasks don't burn your rate limit.
Configurable via polling (client-wide) or per call; pass intervalMs
for a fixed interval. Use meshhub.search() / meshhub.book() +
meshhub.waitForTask() when you want the task id yourself.
Order history — no local order store needed
const page = await meshhub.orders.list({ customer: "user-42", limit: 20 });
const order = await meshhub.orders.get("VMT2");
const timeline = await meshhub.orders.timeline("VMT2");
await meshhub.orders.cancelAndWait("VMT2");Customer profiles — no local customer store needed
MeshHub is the system of record for your customers too. Key every
customer by your own user id (end_user_external_id at booking /
external_id here) and render profile pages straight from MeshHub —
keep only credentials and sessions in your own database:
// Look up a customer by YOUR id (exact match — zero or one result).
const { results } = await meshhub.endUsers.list({ external_id: "user-42" });
const profile = results[0] ?? null;
// Create or update the profile (MeshHub UUID from the lookup above).
await meshhub.endUsers.create({ external_id: "user-42", first_name: "Ada" });
await meshhub.endUsers.patch(profile.id, { phone: "+441234567" });
// Saved cards & payment history for the account page.
const cards = await meshhub.payments.listMethods("user-42");
await meshhub.payments.setDefaultMethod(cards.results[0].id);
const txns = await meshhub.payments.listTransactions({
end_user_external_id: "user-42",
});All list endpoints (orders, end users, payment methods, transactions)
return the same paginated {count, next, previous, results} envelope.
Also available: meshhub.apiKeys, meshhub.carRentals /
meshhub.salons (discovery), meshhub.searchSchema(category) (live
per-category search contract for dynamic forms).
Webhooks
MeshHub pushes order, task, payment, and tracking events to your webhook URL, signed with HMAC-SHA256. Verify against the raw request bytes — a re-serialised JSON body will not match the signature:
import express from "express";
import { parseWebhook, WEBHOOK_SIGNATURE_HEADER } from "@meshhub/sdk";
const app = express();
app.post(
"/webhooks/meshhub",
express.raw({ type: "application/json" }), // raw body — required!
async (req, res) => {
try {
const event = await parseWebhook(
req.body, // Buffer (a Uint8Array) — exactly what was signed
req.header(WEBHOOK_SIGNATURE_HEADER) ?? "",
process.env.MESHHUB_WEBHOOK_SECRET!,
);
switch (event.event_type) {
case "order_status_changed": // confirmed | paid | failed | cancelled
case "task_completed":
case "task_failed":
case "payment_succeeded":
case "payment_failed":
case "payment_refunded":
case "tracking_updated":
break;
}
res.sendStatus(200);
} catch {
res.sendStatus(400); // signature mismatch
}
},
);(The /payments/webhooks/revolut/ path in the API schema is inbound from
the payment provider to MeshHub — your app never calls or receives it.)
Error handling
Every API failure throws a typed subclass of MeshHubError. Branch on the
class (or error.slug) — not on the HTTP status; supplier and payment
failures arrive as HTTP 400.
| Class | slug | Meaning / action |
| ----------------------- | ----------------------- | ------------------------------------------------- |
| ValidationError | validation_error | Fix the request; see error.fields |
| SupplierError | supplier_error | Supplier rejected/failed; see error.supplierError |
| SupplierTimeoutError | supplier_timeout | Supplier slow — retryable |
| PaymentError | payment_error | Charge declined / not payable |
| AuthenticationError | authentication_failed | Missing/invalid API key |
| PermissionDeniedError | permission_denied | Key lacks the required scope |
| NotFoundError | not_found | Unknown id (or another tenant's) |
| RateLimitError | rate_limit_exceeded | Back off error.retryAfterSeconds |
| ServerError | internal_server_error | MeshHub-side — retryable |
Async task failures throw TaskFailedError (with the same typed envelope
at .error) and TaskTimeoutError when polling exceeds its budget.
error.retryable is true where a retry can plausibly succeed.
Synchronous mode — one request, no polling
new MeshHubClient({ ..., executionMode: "sync" }) makes async endpoints
block and return the finished task inline — the whole
search-or-book round trip is a single HTTP request. This is the
simplest way to integrate and a fine default for low-throughput
backends; move to the default async mode (plus webhooks) when you need
to fan out requests or respond faster than the slowest supplier.
You can also switch modes per call, e.g. keep the client async but make one blocking search:
const result = await meshhub.searchAndWait(body, { executionMode: "sync" });Reference
- Full protocol documentation: https://merchant.meshhub.ai/docs/tenant-integration.md
- Machine-readable schema: https://merchant.meshhub.ai/api/schema/?format=json
- Everything else (examples, AI-builder prompt, llms.txt): https://merchant.meshhub.ai/docs/
Developing this SDK
npm ci # install
npm run generate:types # regenerate src/generated/openapi.ts from openapi.json
npm run typecheck && npm run lint && npm test && npm run buildTypes are generated from the repo's OpenAPI document; CI fails if they
drift. Releases: bump version in package.json, tag sdk-v<version>, and
the publish workflow ships it to npm.
