Skip to content

Developer resources

Integrate with Laundrix through its REST API and webhooks, or extend the source code. API access is a plan feature (Premium and Enterprise by default) and is configured per company.

API keys

  1. An owner or admin opens Settings → Integrations and creates an API key with a name and one or more scopes.
  2. The key looks like lk_abc123.secret. It is shown once; only a SHA-256 hash is stored. Revoke and recreate it if lost.
  3. Send it as a Bearer token on every request.
Authorization: Bearer lk_abc123.yourSecretHere
ScopeAllows
orders:readList and read orders (including garment stages and the customer-visible timeline)
orders:writeCreate orders
customers:readList customers
invoices:readList invoices
Pin a key to one customer
A key can be limited to a single customer, for example a hotel’s own system. It can then only see and create that customer’s orders, and a request naming a different customer is refused.

Endpoints

Base URL: your site address, for example https://your-domain/api/v1. Requests and responses are JSON.

Method and pathScopeNotes
GET /api/v1/ordersorders:readFilters: status, customerId. Paging: page, pageSize (max 100).
GET /api/v1/orders/:idorders:readAdds garments (code, description, stage, state) and the visible timeline.
POST /api/v1/ordersorders:writeCreates an order priced by the server. Returns 201.
GET /api/v1/customerscustomers:readFilter: q. Paging as above.
GET /api/v1/invoicesinvoices:readFilters: status, customerId.

Examples

# Orders that are ready
curl -H "Authorization: Bearer $KEY" "https://your-domain/api/v1/orders?status=READY"

# Create an order for pickup and delivery
curl -X POST https://your-domain/api/v1/orders \
  -H "Authorization: Bearer $KEY" -H "content-type: application/json" \
  -d '{
    "customerId": "CUSTOMER_ID",
    "items": [{ "serviceId": "SERVICE_ID", "garmentCategoryId": "CATEGORY_ID", "quantity": 3 }],
    "fulfillment": "PICKUP_DELIVERY",
    "pickup":   { "address": "12 Allen Avenue, Ikeja" },
    "delivery": { "address": "12 Allen Avenue, Ikeja" },
    "priorityCode": "NORMAL",
    "notes": "Ring the bell twice"
  }'

Service and category identifiers are the ones shown for your company in the app (and returned by the public booking catalogue). fulfillment is one of IN_STORE, PICKUP, DELIVERY, PICKUP_DELIVERY.

An order in the response

{
  "id": "…", "orderNumber": "LD-10077", "status": "RECEIVED", "paymentStatus": "UNPAID",
  "priority": "NORMAL", "fulfillment": "PICKUP_DELIVERY", "customerId": "…",
  "receivedAt": "…", "expectedAt": "…", "completedAt": null,
  "totals": { "subtotal": 0, "discount": 0, "tax": 0, "fees": 0, "total": 0, "paid": 0, "balance": 0 },
  "trackingUrl": "https://your-domain/track/ABCD-EFGH-JKLM"
}

Money amounts are integers in the smallest currency unit (kobo for NGN), never floats.

Errors and limits

{ "error": { "code": "FORBIDDEN", "message": "API key lacks scope orders:write" } }
StatusCodeMeaning
401UNAUTHENTICATEDMissing, malformed, revoked or unknown key
402FEATURE_LOCKED, PLAN_LIMIT, SUBSCRIPTION_INACTIVEPlan does not include the API, a limit was reached, or writes are blocked because the subscription lapsed
403FORBIDDENThe key lacks the scope or customer
404NOT_FOUNDNo such record for this company
422VALIDATIONThe body failed validation; the message says why
429RATE_LIMITEDToo many requests. Limits are 300 a minute per IP address and 600 a minute per key.

Webhooks

Create an endpoint under Settings → Integrations with a public https URL and the events you want. A signing secret is generated for it.

EventSent when
order.createdAn order is created
order.readyAll garments of an order are ready
order.completedAn order is handed over or delivered
order.cancelledAn order is cancelled
payment.receivedA payment is confirmed
dispatch.failedA pickup or delivery attempt fails

Delivery

  • A POST with headers x-webhook-timestamp and x-webhook-signature and a JSON body { "type": "...", "createdAt": "...", "data": { ... } }.
  • Respond with any 2xx within 8 seconds. Redirects are not followed.
  • Failures are retried with growing delays (minutes up to six hours), up to six attempts, then marked failed.
  • Deliveries are sent by the scheduled job, so keep the jobs endpoint scheduled.

Verify the signature

The signature is the hex HMAC-SHA256, keyed with your endpoint secret, of timestamp + "." + rawBody. Verify it against the raw body, compare in constant time, and reject old timestamps.

import { createHmac, timingSafeEqual } from "crypto";

export function verify(secret, headers, rawBody) {
  const ts = headers["x-webhook-timestamp"], sig = headers["x-webhook-signature"] ?? "";
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;       // older than 5 minutes
  const expected = createHmac("sha256", secret).update(ts + "." + rawBody).digest("hex");
  return sig.length === expected.length && timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

Treat events as at-least-once: use the order or payment identifier in data to ignore duplicates.

Payment provider webhook

Paystack calls /api/webhooks/paystack. The signature (HMAC-SHA512 of the raw body) is verified, the event is stored under a unique constraint so repeats are ignored, and a payment becomes successful only after the server confirms the amount and currency with Paystack. Set this URL in the Paystack dashboard.

Working with the source

Layout

FolderContents
prisma/schema.prisma, migrations and the demo seed
src/app/Pages (staff app, portal, platform admin, public site, docs), server actions and API routes
src/server/services/Business logic, one module per domain (orders, garments, payments, dispatch, inventory…)
src/server/platform/Cross-company code: provisioning, billing, public lookups, API auth, jobs
src/lib/Permissions, entitlements, tenant-scoped client, settings schema, auth, helpers
tests/Vitest suites, including cross-company isolation and permission tests
scripts/Seed helpers, job runner, platform admin creator, browser tests

Conventions that keep it safe

  • Services take a context. Each takes (ctx, input) and starts with a guard(ctx, permission, …) call that checks the permission, a writable subscription and the plan feature.
  • Use the scoped client. ctx.db adds the company filter and rejects foreign identifiers. Only the platform-level modules listed in the isolation test may use the unscoped database client.
  • Permissions are central. Add new ones to src/lib/permissions.ts and to the role templates; never check role names in code.
  • Money is an integer in minor units. Prices, plans and workflows are data, not constants.
  • Unconfigured providers must report “Integration not configured” rather than fake a result.

Common tasks

TaskCommand
Type-check, lint and testnpm run check
Create a migration while developingnpm run db:migrate
Apply migrations in productionnpm run db:deploy
Run background jobs oncenpm run jobs
Create the platform adminnpm run admin:create -- you@company.com "Your Name"
Browser checks (app running and seeded)npm run smoke && npm run e2e
Tests wipe the database named by TEST_DATABASE_URL. Point it at a dedicated test database, never at your development or production data.

Rebranding

Name, tagline, logo, icon and colours are read from src/lib/brand.ts and can be overridden with environment variables. Each company uploads its own logo in the app. The company behind the product is configured separately with PARENT_COMPANY_* and appears in footers, About and the legal pages.