Embed the intake form on your website

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.

Quick start

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>
Your intake token is a publishable key — it's safe to put directly in your page's HTML, just like a Stripe publishable key. It only lets people submit a request to you; it grants no access to your dashboard or data.

How it works

  1. The script renders a short service-request form inside your <div>.
  2. The form inherits your site's fonts and colors, so it looks like part of your page.
  3. On submit, it sends the request to WorkOrdinate over a secure API call (no page reload).
  4. We match the customer to an existing record by email or phone, or create a new lead.
  5. You're notified instantly, and the request appears in your dashboard.

Configuration

Add any of these optional attributes to the <div>:

AttributeDefaultDescription
data-workordinate-intakeRequired. Your intake token (or custom vanity slug).
data-wo-priorityfalseSet to "true" to let customers choose a priority (normal / urgent / emergency).
data-wo-addressfalseSet to "true" to show the optional service-address field.
data-wo-headingHeading 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-accentA CSS color for the submit button, e.g. "#0b5d3b".
data-wo-successstandard messageCustom 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>

Styling

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.

Appointment booking widget

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:

AttributeDefaultDescription
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.
Slot availability is computed from your business hours, services, capacity, and existing appointments — and every booking is re-checked server-side, so two customers can never claim the last slot at the same time. You also get a hosted booking page at 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.

For developers: the API

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.

Text-only submissions (JSON)

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
}

With photo attachments (multipart FormData)

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>
Do not set 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.

Custom field names

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(/* ... */);
});

Fields

FieldRequiredNotes
customer_nameYesFull name of the person submitting
customer_phoneYesContact phone number
customer_emailYesUsed to match or create a customer record
titleYesShort summary of the request (max 200 chars)
descriptionNoAdditional detail (max 2000 chars)
addressNoService address, if the provider has this option enabled
priorityNonormal | urgent | emergency. If the provider has this option enabled.
photosNoUp to 3 files, JPEG/PNG/WebP, 5 MB each. Only via multipart FormData — not JSON.

Responses

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

CORS is open, so the call works from any website. It is intentionally cookie-less and grants no access to anything but creating a request for this one business.

Spam & abuse

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.

Need a hand?

Tell us where you're embedding the form and what you're trying to do — we respond within one business day.

Send a message