The short version
A vendor pastes two lines onto their own website. They load a small script from us, and that script puts a frame on the page. The request form lives inside the frame, on our web address, so nothing on the host page can read what a visitor types and nothing in the frame can read the host page.
The key in those two lines is public by design. It can do exactly one thing: create one proposal request for one company. It reads nothing, from anywhere, ever.
The one sentence to take away.
What the key is
- Publishable. It ships inside a web page’s HTML. Anyone who can view the page can read it, and that is the design rather than a compromise.
- One capability. Create a proposal request for the one company the key belongs to. There is no second thing it does and no list of permissions to widen: it carries no scopes, because a scope list is a promise that a second capability is coming.
- No read access. Not to requests, not to invoices, not to customers. The one thing presenting it does read is the company’s public trading name, which the form prints at the top so a visitor can see who they are writing to. It cannot be exchanged for anything that reads more.
- Unguessable. 256 bits of randomness, which is a different property from secret. Without it, one company could file requests against another by guessing.
- Its own kind. It begins
hoac_embed_and shares nothing with HOAcrew’s private API credentials, which begin differently and are stored as a one-way hash. Two shapes, so a secret scanner that fires on a leaked private credential stays quiet about a string whose whole job is to be on a public page. - Re-displayable. The console shows it again whenever you ask. Hashing it would protect a database reader from learning something already printed on a public web page, and would cost the vendor their snippet when they rebuild the site in two years.
Where the form runs
Most embedded forms are written into the host page by a script. Ours is not, and the difference is the main security property of the feature.
A form that lives in the host page lives in a document shared with every other script on that page — an analytics tag, a chat widget, a plugin installed years ago. Any of them can read any input in it. A visitor’s name, email, phone and property address are not ours to place in a document with that many tenants, so we put them somewhere else: inside a frame served from our address, behind a boundary the browser enforces rather than a promise our code makes.
The frame is also restricted, in the direction that matters to the host:
- It cannot navigate the host page, cover it, or start a download. It can open one thing in a new tab — the privacy notice printed under the form — because a form collecting a name, an email and an address behind a notice nobody can open is worse than one with no notice at all. That is the only navigation it is allowed, and what opens is a page on hoacrew.com.
- It cannot read the host page’s cookies, storage or forms.
- It talks to the loader only to say how tall it needs to be and that a request was sent. The loader checks both the sending address and the exact frame before acting on either message.
The document inside the frame runs under this policy, which it sets on itself:
The widget’s own Content-Security-Policy
default-src 'none'; base-uri 'none'; form-action 'none'; img-src data:; style-src 'nonce-…'; script-src 'nonce-…'; connect-src 'self'; frame-ancestors <your registered addresses>
Reading down that list: it loads nothing from anywhere by default, cannot be used as a base for a redirect, cannot submit a form anywhere by native means, has no images beyond those built into the page itself, runs only the one script we put there, talks only to us, and may be framed only by the addresses the vendor registered.
The address allow-list
Each key carries a list of up to 10 web addresses. Every time the form is served, we tell the browser which addresses may display it, and the browser refuses anywhere else. Matching is exact — no wildcards, no subdomain patterns, no matching on the end of a name, because every allow-list that has ever been walked past by a lookalike domain was comparing part of a string.
A key with nothing registered displays nowhere. That is the safe direction: a key issued and forgotten is inert rather than universally embeddable.
The intake also checks the address a submission claims to have come from. A claim that is on the list is recorded, so the vendor can see which of their pages produced the request. A claim that is not on the list is refused. A submission that claims nothing is accepted and recorded with no address, because the real enforcement already happened in the browser before the request existed, and refusing on a missing claim would only refuse the honest.
Be exact about what this buys.
What bounds a request sent directly
Four things, in the order they are checked, each cheaper than the next:
- A ceiling per visitor. Counted per minute and per hour on the connection the request came from, before anything else is read.
- A ceiling per key, per minute and per day. Set on each key and shown on the plugin page, so a company that expects volume can raise theirs and a company that does not can lower it. This is the ceiling that decides how bad a bad day can get.
- A single-use token, issued by us. Every served form carries one. It is signed, so it cannot be manufactured; it stops working after 30 minutes; and it is claimed on first use, so a captured submission cannot be replayed. Getting a fresh one means fetching the form again, which is a request that counts against both ceilings. Single use is recorded in the same shared store as the ceilings below, and relaxes the same way if that store is unavailable — see the last section.
- Two checks for automation. A field no person can see or reach, which form-filling scripts complete because they read the markup rather than the layout; and a floor of 4 seconds between the form being served and being sent, measured from our own clock rather than from a number the page reports.
Those last two are guesses, and are treated as guesses. A submission that trips either one is still written and still the vendor’s — filed under Filtered as spam, where one press moves it back. A homeowner who wanted a quote and got a confirmation for a message that was quietly binned is a customer lost by us on the vendor’s behalf, invisible to everybody. Nothing is discarded.
No CAPTCHA, and no plan to add one.
What is recorded
For each request: what the visitor typed, which registered address it came from, and the time. Specifically:
- Name, and an email address or a phone number, or both. One of the two is required, because a request nobody can answer is not a request.
- Community name, property address, the service they picked, and their message. All optional.
- The registered address the form was displayed on, when the browser reported one.
- A one-way fingerprint of the connection’s IP address. The address itself is not kept. The fingerprint is enough to recognize one flooder across many rows and not enough to be an address.
- Which key brought the request in, and the time it arrived.
The form sets no cookies, on the host site or on ours, and does no tracking of any kind. It loads no fonts, no analytics and no other company’s code, so a visitor who fills it in is seen by us and by the vendor, and by nobody else.
The request belongs to the vendor company it was addressed to. Their logins can read it; HOAcrew staff can, for support; no other company can, and the rule is enforced in the database rather than in the page that renders it.
Submitting the form sends no email to anyone — not to the vendor, not to the visitor, not to us. The request appears in the console, which is a place that cannot bounce and that needs nobody’s mail server to be working.
That is a deliberate choice rather than an unfinished one. A public form that mails an address a stranger typed is a way to send mail to addresses that do not exist, on somebody else’s behalf, at whatever rate they like. If a notification is ever added it will be something the vendor turns on for an address of their own.
Who inside your company can change what
Issuing a key, registering an address and revoking a key are publishing acts — they decide what appears on the company’s public website — so they are held by the company’s admin login. HOAcrew staff can do the same for support.
Reading the requests is wider by one step: any login attached to the company can see them, because a request is work for the company and the person who will do the job should be able to see it arrive. What a technician login cannot do is change where the form is published.
Shutting a key off
One press on the plugin page. It takes effect on the very next request, with no waiting period and nothing cached: the form stops rendering everywhere it is installed, and the intake stops accepting that key.
Revoking is permanent. A revoked key is never revived, because a key that can come back is a key whose being off is a setting rather than a fact. Issue a new one and repaste the snippet.
Every request the old key collected stays exactly where it is. Those are the company’s customers, and burning a credential never takes them with it.
Revoking is never withheld.
What this does not protect against
Everything above is accurate. Here is the shape of what is left, because knowing the edge is worth more than a reassuring paragraph.
An automated browser on a registered address
The strongest attack available is a real browser, driven by a script, on a site the vendor genuinely registered. Every check passes honestly: the address is on the list, a real render issues a real token, the hidden field stays empty because automation fills only what it can see, and waiting a few seconds beats the timing floor. At the level of inspection available to an unauthenticated public form, that is indistinguishable from a person, and anyone who claims otherwise is describing a puzzle for your visitors to solve. What holds is the ceilings and the blast radius: the key reads nothing, so the worst outcome is unwanted rows in one inbox and one press to shut the key off.
The page around our frame
A hostile website could put our form on their page and write anything they like directly underneath it. That is a property of being embeddable at all, and no embedded form can fix it from the inside. Two things limit it: the form asks for nothing sensitive — no card, no password, no document — and it names the company it belongs to, so a visitor can see who is receiving what they type.
Shared addresses
An allow-list is only as good as who else is at the addresses on it. Registering a web address that other people’s sites also answer at — a shared page-builder address, for instance — hands the key to every one of them. Register addresses you control. It is also why the Wix guide recommends a button rather than the embed: on that builder the address doing the framing belongs to Wix and is shared with every Wix site.
The counters can fail open
The ceilings are counted in a shared store. If that store were unavailable, the code allows the request rather than refusing it — a deliberate choice, because an outage in a counter must never stop a real homeowner reaching a real company. On hoacrew.com that store is provisioned and the ceilings are live. It is worth knowing which way the failure goes.
The same store holds the single-use record for the form token, so it relaxes on the same outage: with no store reachable, a token is accepted on its signature and its thirty-minute window rather than exactly once. The signature is unaffected — a token still cannot be manufactured — and everything the ceilings bound goes on being bounded by the origin allow-list, which is enforced by the browser and needs no store at all.
No file uploads
A homeowner cannot attach photographs of the property, which is often the first thing a contractor wants to see. A public upload endpoint that anybody on the internet can reach is a different problem with a different bill, and it is not in this version. The conversation continues in the console, where the vendor can ask for photographs directly.
The form looks like ours
The host page’s stylesheet cannot reach inside the frame — that is the same property that stops eleven other scripts reading a homeowner’s email as it is typed. Heading, service list, light or dark, and the color of the send button are the controls. A company with a strong brand system will see a HOAcrew form on their page, and the honest alternative for them is the hosted link behind a button of their own design rather than a near-match sold as a match.