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.
| Form | data-upc-form | What the visitor does | Where it lands | Account needed |
|---|---|---|---|---|
| Contact | contact | Writes a message | A conversation in the business’s inbox | No |
| Request a quote | quote | Describes the job, then confirms their email with a 6-digit code | A Requested quote in Dashboard → Commerce → Quotes → Requests | No |
| Booking request | booking | Picks 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 form | No |
| The business’s own forms | form:<deployment id> | Fills in an application (a puppy application, say), a waiver, or a contract they sign on their own | Dashboard → Paperwork → Forms → Responses | Yes, a free United Pet Club account |
Before you start
Section titled “Before you start”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:
- List the website where the forms will appear, for example
https://www.mybiz.com. The forms show only on listed websites (see Allowed websites). - 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.
- 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.
The snippet
Section titled “The snippet”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>Attributes
Section titled “Attributes”| Attribute | Values | Default | Meaning |
|---|---|---|---|
data-upc-form | contact, quote, booking, form:<deployment id> | (required) | Which form to show. |
data-upc-key | upc_emb_ + 22 characters | (required) | The business’s form key. |
data-upc-mode | inline, button | inline | inline puts the form in the page. button puts a button there that opens the form in a dialog. |
data-upc-label | text | Contact us | The button’s text, in button mode. |
data-upc-locale | a locale such as en-CA, fr-CA, es-MX | the business’s language | The language of the form’s labels and messages. |
data-upc-accent | #rrggbb | the business’s theme | The button and focus colour. |
data-upc-radius | 0 to 24 | the business’s theme | Corner rounding of fields and buttons, in pixels. |
data-upc-font | system, serif, rounded, mono | the business’s theme | The font family. |
data-upc-color-scheme | auto, light, dark | the business’s theme | auto follows the visitor’s device. |
data-upc-title | false | shown | false 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.
Button mode
Section titled “Button mode”<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.
JavaScript API
Section titled “JavaScript API”The loader defines window.UPC:
| Member | Does |
|---|---|
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.version | The 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
Section titled “Events”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.
| Event | When | event.detail |
|---|---|---|
upc:ready | The form has loaded and is ready for the visitor. | { form } |
upc:submitted | The visitor sent the form. | { form, reference } |
upc:close | The visitor closed a dialog (button mode or UPC.open). | { form } |
reference names what the visitor created, so you can show it or count it:
| Form | reference |
|---|---|
contact, booking | The conversation’s id |
quote | The 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.
Example: a Cloudflare Pages site
Section titled “Example: a Cloudflare Pages site”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:
| Website | Covers |
|---|---|
https://www.mybiz.com | The custom domain |
https://mybiz.pages.dev | The project’s production address |
https://*.mybiz.pages.dev | Every 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.comNothing else is needed on your page. The spam check runs inside the frame, not on your page.
Loading the script
Section titled “Loading the script”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-…"andcrossorigin="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.
Styling
Section titled “Styling”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.
Class names
Section titled “Class names”These class names are stable within v1:
| Class | Element |
|---|---|
.upc-root | The form’s outer box |
.upc-title, .upc-intro | The heading and the text under it |
.upc-form | The <form> |
.upc-row | A row holding several fields side by side |
.upc-field, .upc-label, .upc-hint | One field, its label, its help text |
.upc-input, .upc-textarea, .upc-select | The inputs |
.upc-button, .upc-button-secondary, .upc-link-button | The main button, the second button, a button that looks like a link |
.upc-slot, .upc-day-chip | A free time and a day in the booking form |
.upc-error | An error message |
.upc-done, .upc-done-title, .upc-done-body | The “sent” panel |
And these custom properties:
| Variable | Meaning |
|---|---|
--upc-accent | Button and focus colour |
--upc-radius | Corner rounding |
--upc-font | Font stack |
--upc-bg, --upc-fg | Background and text colour |
--upc-muted | Secondary text |
--upc-line | Borders |
--upc-field-bg | Field 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);}What CSS is refused
Section titled “What CSS is refused”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,@namespaceor@charset; - calls
url()with anything other than adata:image/…value, or usesimage-set(),src(),expression(),behavior,-moz-bindingor ajavascript:/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.
Security model
Section titled “Security model”The form runs in a frame
Section titled “The form runs in a frame”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.
Allowed websites
Section titled “Allowed websites”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.comandhttps://www.mybiz.comare two websites; list both if both serve pages. - A leading
*.covers the addresses below a name, never the name itself:https://*.mybiz.pages.devcovershttps://preview.mybiz.pages.devbut nothttps://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.appand 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://localhostandhttp://127.0.0.1on any port. It is off by default.
The key and its replacement
Section titled “The key and its replacement”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.
When the forms are off
Section titled “When the forms are off”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.
Visitor sign-in
Section titled “Visitor sign-in”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.
- The frame shows Sign in or create an account.
- 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.
- United Pet Club shows Continue to {business}. The visitor presses Continue.
- 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.
Limits
Section titled “Limits”| What | Limit |
|---|---|
| Websites per business | 10 |
| Quote requests | 3 per email address per business per day |
| Your own CSS | 20,000 characters |
| Script cache | 1 hour |
| Saved CSS reaching frames | about 1 minute |
Troubleshooting
Section titled “Troubleshooting”| What you see | Why | Fix |
|---|---|---|
An empty frame or the browser’s refused to connect box, and a frame-ancestors refusal in the console | The 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 URL | Preview 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 locally | http://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 malformed | A 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 applied | It 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 scrollbar | Your page sets a fixed height on the placeholder or its parent. | Leave the height free; the frame sizes itself. |