Add your service-request form to any website with a single snippet. When a customer submits, the request lands in your WorkOrdinate dashboard — matched to an existing customer or filed as a new lead automatically. No backend, no login, no app.
Copy your snippet from the dashboard (Accounts → Embed on your website), or build it by hand. Paste it into your page's HTML wherever you want the form to appear:
<div data-workordinate-intake="YOUR_INTAKE_TOKEN"></div>
<script src="https://app.workordinate.com/embed.js" async></script>
<div>.Add any of these optional attributes to the <div>:
| Attribute | Default | Description |
|---|---|---|
data-workordinate-intake | — | Required. Your intake token (or custom vanity slug). |
data-wo-priority | false | Set to "true" to let customers choose a priority (normal / urgent / emergency). |
data-wo-address | false | Set to "true" to show the optional service-address field. |
data-wo-heading | — | Heading rendered above the form. Omit if your page already provides one. |
data-wo-button | "Submit request" | Label for the submit button (e.g. "Book now", "Send inquiry"). |
data-wo-accent | — | A CSS color for the submit button, e.g. "#0b5d3b". |
data-wo-success | standard message | Custom confirmation message shown after a successful submission. |
Example with options:
<div data-workordinate-intake="YOUR_INTAKE_TOKEN"
data-wo-priority="true"
data-wo-accent="#0b5d3b"
data-wo-success="Got it! We'll call you within the hour."></div>
<script src="https://app.workordinate.com/embed.js" async></script>
The form ships almost unstyled on purpose, so it adopts your site's look. To customize further, target these classes in your own CSS:
.wo-intake-form { /* the form wrapper */ }
.wo-field { /* each label + input group */ }
.wo-field input, .wo-field textarea, .wo-field select { }
.wo-submit { /* the submit button */ }
.wo-error { /* per-field error text */ }
.wo-success { /* confirmation message */ }
You can place multiple embeds on one page (each with its own token) — they won't conflict.
If you've enabled online booking (Account Settings → Online booking), you can also embed the appointment picker. Customers choose a service, pick an open time slot from your live availability, and get an instant email confirmation with a calendar invite — the appointment lands in your Schedule view with a linked request.
<div data-workordinate-booking="YOUR_INTAKE_TOKEN"></div>
<script src="https://app.workordinate.com/booking.js" async></script>
It uses the same publishable token as the intake form. Optional attributes:
| Attribute | Default | Description |
|---|---|---|
data-wo-heading | (none) | Heading shown above the picker. |
data-wo-accent | (inherited) | Accent color for the booking button. |
data-wo-success | “You're booked!…” | Custom confirmation message. |
data-wo-service | (none) | Preselect a service by ID, skipping the service step. |
https://app.workordinate.com/book/YOUR_TOKEN if you'd rather link than embed.The intake form and the booking widget can live on the same page — their scripts and styles don't conflict.
The embed script is a thin wrapper around a public, CORS-enabled API. You can post to it directly from your own form — useful when you need full control over layout, field labels, or existing-page styling.
If you don't need photo uploads, post JSON with Content-Type: application/json:
POST https://app.workordinate.com/v1/intake/YOUR_INTAKE_TOKEN
Content-Type: application/json
{
"customer_name": "Sarah Kim",
"customer_phone": "(585) 555-1234",
"customer_email": "sarah@example.com",
"title": "Leaking kitchen faucet",
"description": "Started this morning, getting worse.",
"address": "12 Elm St", // optional
"priority": "urgent" // optional: normal | urgent | emergency
}
To let customers attach photos, submit as multipart/form-data using the browser's FormData API instead of JSON. Add a file input named photos to your form — up to 3 images (JPEG, PNG, or WebP), 5 MB each.
If your form field names already match WorkOrdinate's (customer_name, customer_email, etc.), pass the whole form directly:
<form id="requestForm">
<input name="customer_name" type="text" required>
<input name="customer_phone" type="tel" required>
<input name="customer_email" type="email" required>
<input name="title" type="text" required>
<textarea name="description"></textarea>
<input type="file" name="photos" multiple
accept="image/jpeg,image/png,image/webp">
<button type="submit">Send</button>
</form>
<script>
document.getElementById('requestForm').addEventListener('submit', function (e) {
e.preventDefault();
// Pass the whole form — browser sets the multipart boundary automatically.
// Do NOT set Content-Type manually; that breaks the boundary.
fetch('https://app.workordinate.com/v1/intake/YOUR_INTAKE_TOKEN', {
method: 'POST',
body: new FormData(this),
})
.then(r => r.json())
.then(data => {
if (data.ok) { /* show success */ }
else if (data.errors) { /* show per-field errors */ }
});
});
</script>
Content-Type: multipart/form-data manually. The browser must include the boundary it generates; setting the header yourself strips the boundary and the server rejects the body. Omit the header entirely and let the browser set it.If your form uses different field names (e.g. firstName + lastName instead of customer_name), build FormData manually and append only what you need:
form.addEventListener('submit', function (e) {
e.preventDefault();
const fd = new FormData();
fd.append('customer_name', (get('firstName') + ' ' + get('lastName')).trim());
fd.append('customer_email', get('email'));
fd.append('customer_phone', get('phone'));
fd.append('title', get('reason') || 'Website inquiry');
fd.append('description', get('message'));
// Append each selected photo individually
Array.from(form.querySelector('[name="photos"]').files)
.forEach(file => fd.append('photos', file));
fetch('https://app.workordinate.com/v1/intake/YOUR_INTAKE_TOKEN', {
method: 'POST', body: fd,
}).then(/* ... */);
});
| Field | Required | Notes |
|---|---|---|
customer_name | Yes | Full name of the person submitting |
customer_phone | Yes | Contact phone number |
customer_email | Yes | Used to match or create a customer record |
title | Yes | Short summary of the request (max 200 chars) |
description | No | Additional detail (max 2000 chars) |
address | No | Service address, if the provider has this option enabled |
priority | No | normal | urgent | emergency. If the provider has this option enabled. |
photos | No | Up to 3 files, JPEG/PNG/WebP, 5 MB each. Only via multipart FormData — not JSON. |
A successful submission returns:
{ "ok": true, "matched": false }
Validation problems return 422 with a per-field map you can show next to each input:
{ "ok": false, "errors": { "customer_email": "Please enter a valid email address." } }
Photo upload errors also return 422:
{ "ok": false, "errors": { "photos": "Each photo must be 5 MB or smaller." } }
Other responses: 404 (unknown token), 503 (not accepting requests right now), 429 (too many requests — slow down).
The form includes a hidden honeypot field to silently drop bots, and submissions are rate-limited per visitor. For high-traffic sites that need stronger protection, get in touch — additional controls are on the roadmap.
Tell us where you're embedding the form and what you're trying to do — we respond within one business day.
Send a message