Skip to content

Embed forms on your website

A business on United Pet Club can show its forms on a website it runs itself: a Cloudflare Pages, Netlify or GitHub Pages site, a site built with a static generator, or any page you can paste HTML into. You need no server. Visitors fill the form in on that page, and what they send reaches the business on United Pet Club.

Formdata-upc-formWhat the visitor doesWhere it landsAccount needed
ContactcontactWrites a messageA conversation in the business’s inboxNo
Request a quotequoteDescribes the job, then confirms their email with a 6-digit codeA Requested quote in Dashboard → Commerce → Quotes → RequestsNo
Booking requestbookingPicks a service and a free time (or asks for a suggestion)A booking-request message in the inbox, which the business confirms. No payment is taken in the formNo
The business’s own formsform:<deployment id>Fills in an application (a puppy application, say), a waiver, or a contract they sign on their ownDashboard → Paperwork → Forms → ResponsesYes, a free United Pet Club account

The business sets this up in its Dashboard under Website → Forms on your website. If you are building the site for someone else, ask them to:

  1. List the website where the forms will appear, for example https://www.mybiz.com. The forms show only on listed websites (see Allowed websites).
  2. Turn the forms on (Show the forms on my websites). Turning them on needs the same identity, address and payout verification as publishing a website on United Pet Club; the Dashboard lists any step that is missing.
  3. Copy the code and send it to you. It carries the business’s form key, upc_emb_ followed by 22 letters and digits.

The form key is public. It sits in your page’s HTML and is safe to show, much like a map’s API key restricted to your domain. It only says which business the forms belong to; the list of websites is what stops anyone else from using it.

Forms on your website are free for every business, on any plan.

Put a placeholder where each form should appear, and the loader script once per page, anywhere after or before it:

<div data-upc-form="quote" data-upc-key="upc_emb_XXXXXXXXXXXXXXXXXXXXXX"></div>
<script src="https://www.unitedpetclub.com/embed/v1/upc.js" async></script>

The script finds every element with a data-upc-form attribute and puts the form inside it. Several forms on one page share the one script tag:

<section id="contact">
<div data-upc-form="contact" data-upc-key="upc_emb_XXXXXXXXXXXXXXXXXXXXXX"></div>
</section>
<section id="book">
<div data-upc-form="booking" data-upc-key="upc_emb_XXXXXXXXXXXXXXXXXXXXXX" data-upc-title="false"></div>
</section>
<script src="https://www.unitedpetclub.com/embed/v1/upc.js" async></script>

For one of the business’s own forms, use form: and the form’s deployment id (a GUID). The Dashboard’s form picker puts it in the code for you:

<div data-upc-form="form:3f2b8c1e-7a4d-4e1b-9c55-0d6a2e9f4b10" data-upc-key="upc_emb_XXXXXXXXXXXXXXXXXXXXXX"></div>
AttributeValuesDefaultMeaning
data-upc-formcontact, quote, booking, form:<deployment id>(required)Which form to show.
data-upc-keyupc_emb_ + 22 characters(required)The business’s form key.
data-upc-modeinline, buttoninlineinline puts the form in the page. button puts a button there that opens the form in a dialog.
data-upc-labeltextContact usThe button’s text, in button mode.
data-upc-localea locale such as en-CA, fr-CA, es-MXthe business’s languageThe language of the form’s labels and messages.
data-upc-accent#rrggbbthe business’s themeThe button and focus colour.
data-upc-radius0 to 24the business’s themeCorner rounding of fields and buttons, in pixels.
data-upc-fontsystem, serif, rounded, monothe business’s themeThe font family.
data-upc-color-schemeauto, light, darkthe business’s themeauto follows the visitor’s device.
data-upc-titlefalseshownfalse hides the form’s heading, for when your page has its own.

Attributes on the element override the theme the business saved in the Dashboard, for that one form only. A value outside the allowed range is not used: that one setting falls back to the plain default (an orange accent, 10 px corners, the system font, auto), so keep to the values in the table.

<div data-upc-form="contact" data-upc-key="upc_emb_XXXXXXXXXXXXXXXXXXXXXX"
data-upc-mode="button" data-upc-label="Ask us a question"></div>

The button is an ordinary <button> on your page with the class upc-launcher-button, so your own stylesheet styles it. Pressing it opens the form in a modal dialog. The dialog closes with its × button or the Esc key only, never with a click outside it, so a half-typed request can’t vanish by accident.

The loader defines window.UPC:

MemberDoes
UPC.mount(element, options)Puts a form in element. options override the element’s data-upc-* attributes. Returns the frame (inline) or the button (button mode), or null when the element already holds a form or the options are invalid.
UPC.open(form, options)Opens form in a dialog straight away, with no button on the page. options must include key. Returns the hidden element the events fire on.
UPC.scan()Mounts every [data-upc-form] element not mounted yet. The loader runs it once when the page loads; call it again after you add placeholders to the page.
UPC.versionThe loader’s version, "1.0.0".

The options object uses the attribute names in camel case without the prefix: form, key, mode, label, locale, accent, radius, font, colorScheme, title (false hides the heading), plus frameTitle for the frame’s accessible title (default United Pet Club form).

A mounted element gets data-upc-mounted="1", so calling UPC.scan() or UPC.mount() twice never adds a second form. That makes it safe to call UPC.scan() after every client-side route change in a single-page app.

// A form added after the page loaded (a single-page app, a tab that renders later):
const host = document.querySelector("#quote-slot");
UPC.mount(host, { form: "quote", key: "upc_emb_XXXXXXXXXXXXXXXXXXXXXX", locale: "fr-CA" });
// Open the booking form from your own button:
document.querySelector("#book-now").addEventListener("click", () => {
UPC.open("booking", { key: "upc_emb_XXXXXXXXXXXXXXXXXXXXXX", title: false });
});

Because the script loads with async, window.UPC may not exist yet when your own script runs. Wait for it before calling the API:

<script src="https://www.unitedpetclub.com/embed/v1/upc.js" async id="upc-forms"></script>
<script>
function whenUpc(fn) {
if (window.UPC) fn(window.UPC);
else document.getElementById("upc-forms").addEventListener("load", () => fn(window.UPC));
}
whenUpc((UPC) => UPC.scan());
</script>

Events are CustomEvents dispatched on the placeholder element (for UPC.open, on the element it returns). They bubble, so one listener on document hears every form on the page.

EventWhenevent.detail
upc:readyThe form has loaded and is ready for the visitor.{ form }
upc:submittedThe visitor sent the form.{ form, reference }
upc:closeThe visitor closed a dialog (button mode or UPC.open).{ form }

reference names what the visitor created, so you can show it or count it:

Formreference
contact, bookingThe conversation’s id
quoteThe quote number
form:<id>The submission’s id
document.addEventListener("upc:submitted", (event) => {
const { form, reference } = event.detail;
// Your own analytics, a thank-you message elsewhere on the page, a redirect, ...
console.log(`Sent ${form}`, reference);
});
document.addEventListener("upc:close", (event) => {
console.log(`Closed ${event.detail.form}`);
});

The frame tells your page nothing else: not what the visitor typed, not their email, not whether they are signed in.

A plain static site deployed to Cloudflare Pages as the project mybiz, with the custom domain www.mybiz.com.

contact.html:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Contact | My Biz Grooming</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<main>
<h1>Get in touch</h1>
<div data-upc-form="contact" data-upc-key="upc_emb_XXXXXXXXXXXXXXXXXXXXXX" data-upc-title="false"></div>
<h2>Need a price first?</h2>
<div data-upc-form="quote" data-upc-key="upc_emb_XXXXXXXXXXXXXXXXXXXXXX"
data-upc-mode="button" data-upc-label="Request a quote"></div>
</main>
<script src="https://www.unitedpetclub.com/embed/v1/upc.js" async></script>
</body>
</html>

In Your websites the business lists:

WebsiteCovers
https://www.mybiz.comThe custom domain
https://mybiz.pages.devThe project’s production address
https://*.mybiz.pages.devEvery preview and branch address, such as https://3f9a1c2b.mybiz.pages.dev

While you build the site with wrangler pages dev or any local server, the business turns on Let my computer show the forms while I build my site, and the forms show on http://localhost and http://127.0.0.1 on any port. Ask them to turn it off again when the site is live.

If your site sends its own Content-Security-Policy (for example from a _headers file), allow the loader and its frame:

/*
Content-Security-Policy: default-src 'self'; script-src 'self' https://www.unitedpetclub.com; frame-src https://www.unitedpetclub.com

Nothing else is needed on your page. The spam check runs inside the frame, not on your page.

https://www.unitedpetclub.com/embed/v1/upc.js is served with Cache-Control of one hour and Access-Control-Allow-Origin: *. Fixes and additions to v1 reach your page within the hour; a change that would break existing snippets gets a new path.

  • Load it from www.unitedpetclub.com. The loader opens its frames on the address it was loaded from, so a copy served from your own domain looks for the forms on your domain and finds nothing.
  • Subresource Integrity. If your policy needs it, add integrity="sha384-…" and crossorigin="anonymous". When v1 is updated the file no longer matches your hash and the browser refuses to run it, so the forms stop showing until you put the new hash in.

Every form has the business’s theme, set in the Dashboard under Look: Button colour, Corner rounding, Font and Colour scheme. The data-upc-* attributes override it for one form.

The form runs in a frame, so your page’s stylesheet can’t reach inside it. To restyle the form itself, the business pastes CSS into Your own CSS in the Dashboard. It applies to every form on every listed website, and saved CSS shows in frames within about a minute.

These class names are stable within v1:

ClassElement
.upc-rootThe form’s outer box
.upc-title, .upc-introThe heading and the text under it
.upc-formThe <form>
.upc-rowA row holding several fields side by side
.upc-field, .upc-label, .upc-hintOne field, its label, its help text
.upc-input, .upc-textarea, .upc-selectThe inputs
.upc-button, .upc-button-secondary, .upc-link-buttonThe main button, the second button, a button that looks like a link
.upc-slot, .upc-day-chipA free time and a day in the booking form
.upc-errorAn error message
.upc-done, .upc-done-title, .upc-done-bodyThe “sent” panel

And these custom properties:

VariableMeaning
--upc-accentButton and focus colour
--upc-radiusCorner rounding
--upc-fontFont stack
--upc-bg, --upc-fgBackground and text colour
--upc-mutedSecondary text
--upc-lineBorders
--upc-field-bgField background
.upc-root {
--upc-bg: transparent;
--upc-line: #d6d3d1;
}
.upc-button {
text-transform: uppercase;
letter-spacing: 0.04em;
}
.upc-slot[aria-pressed="true"] {
outline: 2px solid var(--upc-accent);
}

The CSS is checked when the business saves it, and again by United Pet Club before every frame renders it. A refused save names the line and the reason (Line 4: …). It is refused when it:

  • uses @import, @font-face, @namespace or @charset;
  • calls url() with anything other than a data:image/… value, or uses image-set(), src(), expression(), behavior, -moz-binding or a javascript: / vbscript: URL;
  • contains < or control characters;
  • leaves a brace, a string or a comment open;
  • is longer than 20,000 characters.

The frame’s own Content-Security-Policy blocks any outside load anyway, so web fonts and images from other addresses never load in a form. Use one of the four theme fonts, or a font the visitor’s device already has.

The privacy line under the form and the Powered by United Pet Club mark always stay visible. They sit in a cascade layer that the business’s CSS can’t override.

The loader adds an <iframe> whose page is served by www.unitedpetclub.com. What a visitor types goes from that frame to United Pet Club and never passes through your page, so scripts on your page (your analytics, a chat widget, an extension) can’t read it. The loader reads nothing from your page and sends nothing from it other than your page’s origin, which the frame needs to check where it is shown.

The frame is sandboxed: it can run its form and open United Pet Club in a new tab or window, and it can’t navigate your page. It grows and shrinks to fit its form by sending its height to the loader with postMessage; the loader accepts messages only from https://www.unitedpetclub.com and only from a frame it created itself.

United Pet Club tells the browser where each business’s forms may appear, with the frame’s Content-Security-Policy frame-ancestors. The browser enforces it: on a website that isn’t listed, the frame doesn’t show at all, whatever the page’s HTML says. A website the business lists is an origin:

  • https only, written as https:// and the host, with a port only if the site uses one: https://www.mybiz.com. No path.
  • Exact. https://mybiz.com and https://www.mybiz.com are two websites; list both if both serve pages.
  • A leading *. covers the addresses below a name, never the name itself: https://*.mybiz.pages.dev covers https://preview.mybiz.pages.dev but not https://mybiz.pages.dev. It is allowed only below the business’s own name. It is refused right below a shared host (*.pages.dev, *.github.io, *.netlify.app and the like) or a registry (*.co.uk, *.com), because that would cover everyone else’s sites too.
  • Up to 10 websites per business.
  • Your own computer. Let my computer show the forms while I build my site allows http://localhost and http://127.0.0.1 on any port. It is off by default.

A form key on its own does nothing on a website that isn’t listed. If a business wants every old snippet to stop at once, it presses Replace key: the old key stops showing the forms, and the new code must be pasted on every page.

Changes the business makes (the list of websites, the on/off switch, a new key, the theme and CSS) reach the frames within about a minute.

While the forms are turned off, after the key was replaced, for an unknown key, and while the business’s profile is suspended, United Pet Club tells the browser not to show the frame on any website. Your page then shows an empty frame (or the browser’s own refused to connect box), and the console reports a frame-ancestors refusal. A visitor who already had the form open when it was switched off sees This form isn’t available right now. when they send it.

  • Cloudflare Turnstile runs inside the frame. Most visitors never see it; a few are asked to tick a box.
  • Limits per visitor and per business apply to every form.
  • Quote requests need the 6-digit code sent to the visitor’s email, and a visitor can send 3 quote requests per email address to one business per day.

Contact, quote and booking forms need no account. The business’s own forms (applications, waivers, contracts) need a free United Pet Club account, because the answers belong to a member.

  1. The frame shows Sign in or create an account.
  2. Pressing it opens United Pet Club in a window of its own: a pop-up on a computer, a new tab on a phone. The visitor’s password is typed there, on United Pet Club, never on your website.
  3. United Pet Club shows Continue to {business}. The visitor presses Continue.
  4. The window closes and the frame is signed in: Signed in as {name} · Sign out.

The sign-in lets the frame deal with that one business only: the visitor’s answers to its forms. It gives your page nothing; your page never sees a token or the visitor’s name.

A form that asks for files or for one of the visitor’s pets can’t be filled in inside the frame. It shows Continue on United Pet Club, which opens the form there in a new tab.

When a form carries a contract the visitor signs on their own, the frame says One more step: sign the agreement. after they send it, and Review and sign opens the signing step in the same frame.

WhatLimit
Websites per business10
Quote requests3 per email address per business per day
Your own CSS20,000 characters
Script cache1 hour
Saved CSS reaching framesabout 1 minute
What you seeWhyFix
An empty frame or the browser’s refused to connect box, and a frame-ancestors refusal in the consoleThe page’s address isn’t one of the listed websites, the forms are turned off, or the key is wrong, was replaced or belongs to a suspended profile.Check that the exact origin is in Your websites (www., the port), that Show the forms on my websites is on, and copy the code again from Website → Forms on your website.
This form isn’t available (or …right now. when sending)The form was switched off, or the key replaced, after the page loaded.Reload the page once the business has turned the forms back on, or paste the new code.
The form shows on the live site but not on a preview URLPreview addresses (Cloudflare Pages <hash>.<project>.pages.dev, Netlify deploy previews) are different origins.List https://*.yourproject.pages.dev (or the host’s preview pattern under your own name).
The form shows nowhere while you work locallyhttp://localhost isn’t allowed by default.Turn on Let my computer show the forms while I build my site.
The console says [United Pet Club forms] unknown data-upc-form or data-upc-key is missing or malformedA typo in an attribute.data-upc-form is contact, quote, booking or form: and a GUID; the key is upc_emb_ and 22 characters.
Your browser blocked the sign-in window. Allow pop-ups for this site, then try again.The browser stopped the sign-in pop-up.The visitor allows pop-ups for your site and presses Sign in again.
We couldn’t confirm you’re a person. Reload the page and try again.The spam check didn’t finish, often because a blocker stopped Cloudflare’s script inside the frame.Reload. If it keeps happening, the visitor turns off the blocker for the page.
You’ve sent the most requests for today. Try again tomorrow.The 3-a-day quote limit for that email address.Wait a day, or use the contact form.
The business’s CSS isn’t appliedIt was refused on save, or it is less than a minute old.Check the Dashboard for the refusal line; wait a minute and reload.
The form is cut off or has a scrollbarYour page sets a fixed height on the placeholder or its parent.Leave the height free; the frame sizes itself.