Skip to content

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.

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.

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:

Terminal window
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:

Terminal window
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:

Terminal window
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.

Terminal window
curl https://api.<domain>/v1/resources/<RESOURCE_ID> \
-H "Authorization: Bearer sk_test_..."
{
"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:

Terminal window
curl "https://api.<domain>/v1/room-types?vertical=equipment" \
-H "Authorization: Bearer sk_test_..."
# → 200 { "roomTypes": [ { "id": "<ROOM_TYPE_ID>", "vertical": "equipment", ... } ], "nextCursor": null }

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.

Terminal window
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"
}'

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.

StatuscodeMeaning
400not_equipmentThis room type is lodging, not equipment — use POST /v1/quotes
400invalid_durationto isn’t a whole number of days after from
404room_type_not_found / rate_plan_not_foundCheck the ID and the tenant’s key
409unavailableEvery unit of this room type is booked for the window
422no_rental_tiersThis rate plan has no daily/weekly/monthly tiers configured yet
422fx_rate_unavailableNo FX rate for the currency you asked for

POST /v1/holds is the same endpoint lodging uses — nothing rental- specific about its shape except one optional field.

Terminal window
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" }
}'

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.

Identical to lodging from here — see Verify a guest and confirm a hold. Two things worth knowing before you promise them to a customer:

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-resource is 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-id is 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-lang accepts id (Bahasa Indonesia) or en.

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.

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