Rent out equipment through the API (WordPress or WooCommerce)
This guide is for an equipment-rental tenant — floor scrubbers, sweepers, vacuums, that kind of gear — wiring rental bookings into their own WordPress or WooCommerce site. It assumes Quickstart and Authentication already make sense; this page covers what’s different about rental: tiered pricing instead of per-night, and allocating one of several interchangeable physical units instead of one specific room.
Before you start
Section titled “Before you start”Same key pattern as any tenant: your operator contact gives you an
sk_test_… and a pk_test_… key after onboarding. See
Authentication if you haven’t seen these yet.
Set up rental inventory
Section titled “Set up rental inventory”Setup is entirely self-serve with a secret key (or an operator session) —
four calls: create the equipment model, add physical units, price it, set the
tiers. (These are operator/sk_ endpoints — never call them from the widget or
with a publishable key.)
1 — Create the equipment room type (the model). vertical: "equipment" is
what makes it a rental:
curl -X POST https://api.your-booking-engine.example/v1/room-types \ -H "Authorization: Bearer $SECRET_KEY" -H "Content-Type: application/json" \ -d '{"code":"t300-scrubber","name":"Tennant T300 Scrubber","vertical":"equipment"}'# → 201 { "id": "<room-type-id>", ... } (409 duplicate_code if the code is taken)2 — Add physical units (one call per unit in the fleet). Each is a
resource under the room type:
curl -X POST https://api.your-booking-engine.example/v1/resources \ -H "Authorization: Bearer $SECRET_KEY" -H "Content-Type: application/json" \ -d '{"kind":"equipment","name":"T300 unit #1","roomTypeId":"<room-type-id>"}'# → 201 { "id": "<resource-id>", ... } (404 room_type_not_found if the room type isn't yours)3 — Create a rate plan for the model via
PUT /v1/resources/{resourceId}/rates (see the rates
reference) — this returns a rate-plan ID.
4 — Set the rental tiers (daily / weekly / monthly) on that rate plan.
Money is minor-unit strings; a longer tier must never cost more per day than a
shorter one (422 invalid_tiers otherwise). Set weekly/monthly to null to
offer only shorter tiers:
curl -X PUT https://api.your-booking-engine.example/v1/rate-plans/$RATE_PLAN_ID/rental-tiers \ -H "Authorization: Bearer $SECRET_KEY" -H "Content-Type: application/json" \ -d '{"dailyMinorUnits":"500000","weeklyMinorUnits":"3000000","monthlyMinorUnits":"10000000"}'Once the room type, units, rate plan, and tiers exist, everything below is real API you call yourself.
1. Look up the room type
Section titled “1. Look up the room type”curl https://api.<domain>/v1/resources/<RESOURCE_ID> \ -H "Authorization: Bearer sk_test_..."const { data: resource, error } = await client.GET("/v1/resources/{id}", { params: { path: { id: "<RESOURCE_ID>" } },});if (error) throw error;{ "id": "<RESOURCE_ID>", "kind": "equipment", "name": "Floor Scrubber — Model X", "capacity": 1, "currency": "IDR", "slug": "floor-scrubber-model-x", "photoUrls": [], "roomType": { "id": "<ROOM_TYPE_ID>", "code": "SCRUB-X", "name": "Floor Scrubber X", "maxOccupancy": 1, "maxAdults": 1, "maxChildren": 0, "vertical": "equipment" }, "ratePlans": [ { "id": "<RATE_PLAN_ID>", "code": "STD", "name": "Standard", "currency": "IDR", "isRefundable": true, "minStayNights": 1, "maxStayNights": null, "closedToArrival": false, "closedToDeparture": false, "enabled": true } ]}roomType.vertical: "equipment" is how you tell a rental room type from a
lodging one. capacity/maxOccupancy are lodging-shaped fields that don’t
carry rental meaning — ignore them here. GET /v1/resources lists every
physical unit of your fleet; each one repeats the same roomTypeId — the
room type, not the resource, is what you quote against.
To list your models rather than individual units, call
GET /v1/room-types?vertical=equipment (operator/sk_ key, cursor-paginated).
It returns every equipment room type you own — including ones you’ve created
but not yet added units to — which is the id you’ll quote and embed against:
curl "https://api.<domain>/v1/room-types?vertical=equipment" \ -H "Authorization: Bearer sk_test_..."# → 200 { "roomTypes": [ { "id": "<ROOM_TYPE_ID>", "vertical": "equipment", ... } ], "nextCursor": null }2. Quote the rental
Section titled “2. Quote the rental”POST /v1/quotes/rental is a separate endpoint from the lodging
POST /v1/quotes — same auth (quote:read, either key kind), different
body: roomTypeId instead of resourceId, no partySize.
curl -X POST https://api.<domain>/v1/quotes/rental \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "roomTypeId": "<ROOM_TYPE_ID>", "ratePlanId": "<RATE_PLAN_ID>", "from": "2026-09-01T00:00:00Z", "to": "2026-10-04T00:00:00Z" }'const { data: quote, error } = await client.POST("/v1/quotes/rental", { body: { roomTypeId: "<ROOM_TYPE_ID>", ratePlanId: "<RATE_PLAN_ID>", from: "2026-09-01T00:00:00Z", to: "2026-10-04T00:00:00Z", },});if (error) throw error;Response, worked against a rate plan priced at Rp500,000/day, Rp3,000,000/week, Rp10,000,000/month (the pricing engine’s own test fixture for exactly this window):
{ "quoteId": "...", "roomTypeId": "<ROOM_TYPE_ID>", "ratePlanId": "<RATE_PLAN_ID>", "from": "2026-09-01T00:00:00Z", "to": "2026-10-04T00:00:00Z", "availableUnits": 3, "priceBreakdown": { "durationDays": 33, "months": 1, "weeks": 0, "days": 3, "totalMinorUnits": "11500000", "currency": "IDR" }, "fx": null, "displayTotal": { "currency": "IDR", "amountMinorUnits": "11500000" }, "signedQuoteToken": "eyJhbGciOi...", "expiresAt": "2026-09-01T00:15:00.000Z"}33 days is priced as one 30-day month plus 3 daily blocks
(Rp10,000,000 + Rp1,500,000), never two months — the engine always picks
the cheapest whole-block cover for the exact duration, and a longer rental
never costs less per day than a shorter one. from/to must be a whole
number of days apart; a duration that isn’t is 400 invalid_duration.
availableUnits is the free-unit count across your whole fleet of that
room type, at quote time — informational, the same way a lodging quote’s
price is informational until you hold it. IDR has no decimal places (an
amount of 11500000 is eleven-and-a-half-million rupiah, not
rupiah-and-cents) — same money-as-a-string rule as
everywhere else on this API, just a currency where it matters more to get
right.
| Status | code | Meaning |
|---|---|---|
| 400 | not_equipment | This room type is lodging, not equipment — use POST /v1/quotes |
| 400 | invalid_duration | to isn’t a whole number of days after from |
| 404 | room_type_not_found / rate_plan_not_found | Check the ID and the tenant’s key |
| 409 | unavailable | Every unit of this room type is booked for the window |
| 422 | no_rental_tiers | This rate plan has no daily/weekly/monthly tiers configured yet |
| 422 | fx_rate_unavailable | No FX rate for the currency you asked for |
3. Hold a unit
Section titled “3. Hold a unit”POST /v1/holds is the same endpoint lodging uses — nothing rental-
specific about its shape except one optional field.
curl -X POST https://api.<domain>/v1/holds \ -H "Authorization: Bearer sk_test_..." \ -H "Idempotency-Key: <a-uuid-you-generate>" \ -H "Content-Type: application/json" \ -d '{ "signedQuoteToken": "eyJhbGciOi...", "delivery": { "address": "Jl. Sudirman No. 1, Jakarta" } }'const { data: hold, error } = await client.POST("/v1/holds", { headers: { "Idempotency-Key": "<a-uuid-you-generate>" }, body: { signedQuoteToken: quote.signedQuoteToken, delivery: { address: "Jl. Sudirman No. 1, Jakarta" }, },});if (error) throw error;delivery is optional and rental-only — lodging ignores it entirely. Only
address is required; deliveryAt/collectionAt are optional and purely
informational for your own logistics. They do not extend the held
window — the rental period you quoted already covers the unit being away
from the fleet, so there’s nothing extra to reserve.
You don’t send a roomTypeId or pick which physical unit — the engine
allocates a free one at hold time. If every unit sold out between your
quote and your hold, you get 409 no_unit_available instead of a hold —
handle it the same way as any other sold-out response: re-quote and, if
you still want the dates, try again. Same Idempotency-Key retry rule as
lodging: a timed-out request retried with the same key returns the same
hold, never a duplicate.
4. Verify, pay, confirm
Section titled “4. Verify, pay, confirm”Identical to lodging from here — see Verify a guest and confirm a hold. Two things worth knowing before you promise them to a customer:
Embed the widget (rental mode)
Section titled “Embed the widget (rental mode)”The embeddable widget — see the lodging version —
has a rental mode: a date-range picker over the tiered price breakdown
above, plus the optional delivery field, talking only to public pk_
endpoints and Shadow-DOM isolated so it drops into a WordPress page the
same way. Same publishable-key and domain-allowlist rules apply unchanged.
Paste this into a WordPress Custom HTML block (not a Paragraph block —
WordPress strips <script> from paragraphs) or your theme’s Text/HTML view:
<div data-booking-widget data-key="pk_live_your_publishable_key" data-mode="rental" data-resource="<equipment-room-type-uuid>" data-rate-plan-id="<rate-plan-uuid>" data-api-base="https://<your-api-host>" data-lang="id"></div><script src="https://docs.booking-engine.davidvalentino.com/widget/v1.js" async></script>data-mode="rental"selects the equipment vertical. The only other recognized value is"lodging"(the default) — omit the attribute entirely and existing lodging snippets are unaffected. Any other value (a typo like"Rental") is rejected at mount with a misconfiguration message, not silently treated as lodging.data-resourceis the equipment room-type id in rental mode (the model, not a single unit — the engine allocates a free unit at hold time).data-rate-plan-idis required in rental mode; the widget shows a clear misconfiguration message at mount if it’s missing, rather than failing on the guest’s first click.data-langacceptsid(Bahasa Indonesia) oren.
The guest picks a date range, sees the tiered total, optionally enters a delivery address + drop-off/pickup times, and pays through the same hosted checkout as lodging. The publishable key never sees PII beyond what the guest types into that one hold request.
What this doesn’t do yet
Section titled “What this doesn’t do yet”- No WooCommerce product-sync plugin — availability sync is the calendar API only.
- No security deposit / damage hold.
- No recurring or monthly billing — one full charge, up front.
- No WhatsApp or SMS channel for guest verification — email only.
Next steps
Section titled “Next steps”- Quickstart — the base flow this guide builds on.
- Verify a guest and confirm a hold — the shared confirmation flow.
- Calendar and iCal — sync availability into your own catalogue.
- Errors — the full error shape and status codes.