Booking API course · updated 2026-10-05
Lesson 12: a complete example in plain JavaScript
One HTML page that loads sizes, quotes, validates, starts the order, takes the card and confirms. Paste it, replace the key, add your domain in the app, and you have a working booking. Every function maps to one lesson.
Before you start
- Lessons 2 to 8
- ## The page
```
<div id="sizes"></div>
<input id="days" type="number" value="7" min="1" max="365">
<input id="date" type="date">
<input id="address" placeholder="123 Main St"> <input id="zip" placeholder="ZIP">
<input id="name" placeholder="Full name"> <input id="email" placeholder="Email"> <input id="phone" placeholder="Phone">
<label><input id="terms" type="checkbox"> I accept the rental agreement</label>
<div id="quote"></div>
<div id="payment-element"></div>
<button id="pay">Pay</button>
<div id="result"></div>
<script src="https://js.stripe.com/v3/"></script>
``` - ## The client
```
const API = "https://gcwyoiihrupbfqqlcurh.supabase.co/functions/v1/booking-api/v1";
const KEY = "dc_pk_live_..."; // your publishable key; add this page's domain in the app
const headers = { Authorization:Bearer ${KEY}, "Content-Type": "application/json" };
async function api(path, body) {
const res = await fetch(API + path, body ? { method: "POST", headers, body: JSON.stringify(body) } : { headers });
const json = await res.json();
if (!res.ok) throw Object.assign(new Error(json.error?.message || res.statusText), { code: json.error?.code, field: json.error?.field });
return json.data;
}
let sizeId = null, intent = null, stripe = null, elements = null;
(async () => {
const sizes = await api("/sizes");
document.getElementById("sizes").innerHTML = sizes.map((s) =><button data-id="${s.id}">${s.label} from $${s.price_from}</button>).join("");
document.querySelectorAll("#sizes button").forEach((b) => b.onclick = () => { sizeId = b.dataset.id; refreshQuote(); });
document.getElementById("days").onchange = refreshQuote;
})();
async function refreshQuote() {
if (!sizeId) return;
const q = await api("/quote", { size_id: sizeId, rental_days: Number(document.getElementById("days").value) });
document.getElementById("quote").textContent =Total $${q.total} + processing fee $${q.processing_fee} = $${q.gross_total};
}
``` - ## Start, pay, confirm
```
document.getElementById("pay").onclick = async () => {
const out = document.getElementById("result");
try {
if (!intent) {
intent = await api("/intents", {
size_id: sizeId, rental_days: Number(document.getElementById("days").value),
delivery_date: document.getElementById("date").value,
delivery_address: document.getElementById("address").value, delivery_zip: document.getElementById("zip").value,
customer: { name: document.getElementById("name").value, email: document.getElementById("email").value, phone: document.getElementById("phone").value },
terms_accepted: document.getElementById("terms").checked,
});
stripe = Stripe(intent.stripe.publishable_key, intent.stripe.account ? { stripeAccount: intent.stripe.account } : {});
elements = stripe.elements({ clientSecret: intent.client_secret });
elements.create("payment").mount("#payment-element");
out.textContent =Enter your card. You will be charged $${intent.amount.gross_total}.;
return; // second click pays
}
const { error } = await stripe.confirmPayment({ elements, confirmParams: { return_url: location.href }, redirect: "if_required" });
if (error) { out.textContent = error.message; return; }
const c = await api("/confirm", { session_id: intent.session_id, payment_intent_id: intent.payment_intent_id });
out.textContent = c.state === "held" ?Received, under review. Reference ${c.order_number}:Booked! Order ${c.order_number};
} catch (e) {
out.textContent = e.message; // 409 messages are written for the customer
}
};
``` - ## What to add for production
A date picker that greys out GET /availability closed days; POST /check on blur of date, ZIP and promo; the rental agreement text from GET /company; your Google Ads or Meta Pixel conversion after /confirm; GET /sessions polling on the return_url page; and a friendly fallback with the company's phone when booking.is_online or payments_ready is false. - ## Materials in the example
```
const mats = await fetch(${BASE}/materials, { headers }).then(r => r.json());
if (mats.data.enabled) {
// ask the material first; keep only the sizes it allows (sizes[] when size_policy is listed, else all)
const m = mats.data.materials.find(x => x.slug === chosenSlug);
// then quote and start the order with material (and material_acknowledged when m.requires_ack)
body.material = m.slug; if (m.requires_ack) body.material_acknowledged = true;
}
```
On 409 material_not_allowed read err.error.details.allowed_sizes and offer those sizes.
What happens next
- The same skeleton serves junk removal: swap /sizes for /junk/config loads, /quote for /junk/quote, /intents for /junk/intents and /confirm for /junk/confirm.
Troubleshooting
The Payment Element does not render.
Stripe.js must be loaded before you call Stripe(), the client_secret must come from the same /intents response, and for Canadian companies stripeAccount is required.
Related guides
- Lesson 13: React, Next.js and AI builders (Claude, Lovable, Codex)
- Lesson 9: junk removal with the Booking API (/junk/*)
© 2026 Dumpster Controls. All rights reserved. Made in the USA.
Frequently asked questions
Is Dumpster Controls really free?
Yes. The software is free: dispatch, online booking, the driver app, invoicing, the Tresha AI assistant and every other feature, with no monthly fee, no trial period and no credit card to sign up. The only cost on the free plan is optional card processing when a customer pays by card through the platform: 2.99% plus $3.99 per transaction on the free plan. An optional Unlimited plan at $169 per month lowers that to 2.99% plus $0.30. Prices as published on dumpstercontrols.io/pricing on 2026-09-23.
Do you charge per driver, per truck or per order?
No. There is no per-driver, per-truck, per-user or per-order fee, and no order limit. A company with one truck and a company with twenty pay the same for the software: nothing.
Is there a contract?
No. There is no contract, no minimum term and no setup fee. You create the account yourself, and on the free plan there is nothing to cancel because nothing is billed. The optional Unlimited plan is billed month to month.
Which countries and languages are supported?
Dumpster Controls serves hauling companies in the United States and Canada. The app interface and the Tresha AI assistant are available in English, Spanish and Portuguese. The public pages, such as the blog, the help center and the landfill finder, are in English.
How do I switch from another dumpster software?
Create a free account at dumpstercontrols.io/login, with no sales call and no credit card. Then import your customers from a CSV file using the template provided in the app; past orders can also be imported from a CSV. Container sizes and pricing are set up in Settings. The landfill database, with 1,750 active US and Canadian landfills as counted on 2026-10-01, is already loaded, so disposal sites do not need to be typed in. Step-by-step guides are at dumpstercontrols.io/help.