Legal
Privacy Notice
What we collect and why, who else touches it, how long it stays, and — in its own section, because it is the question that matters most — exactly what the widget does and does not record about the people who visit your website.
Last updated 1 October 2026. Questions about this document go to privacy@thelatchkey.in.
This document is a draft and has not been through legal review
Everything below about what the software does is accurate and was checked against the code. What is missing is the part only the company can supply, and until it is supplied this page should not be presented to a customer as final:
registeredName— who the contract is withregisteredAddress— where to send a legal or privacy noticegoverningLaw— which law governs the termsforum— where a dispute is heardliabilityCap— the limit on what can be claimeddatabaseProvider— who hosts the database your data sits in
They are set in one place, LEGAL_ENTITY in latchkey-core/src/legal.ts, and this notice removes itself once they are.
The short version
LatchKey is an accessibility testing and remediation tool. To do its job it loads websites, screenshots them, stores what it found, and runs an accessibility menu on the sites of customers who install it. That means three different kinds of people appear in this notice, and they are owed different things:
- Customers and people who visit this site. We are the controller. We hold your account details, what you scanned, and the email we exchanged.
- The websites you ask us to audit. We are your processor. We load and store copies of pages you told us we may test, on your instructions.
- The people who visit a site running our widget. The site owner is the controller and we are their processor. The widget sets no cookies, stores a visitor’s preferences in their own browser, and sends us a cookie-free beacon with no IP address, no user agent, no assistive-technology signal and no page content in it.
We do not sell personal information, we do not share it for advertising, and we run no advertising or cross-site tracking on any surface. The cookie policy lists every cookie there is; browsing this site while signed out sets none at all.
What we collect, and why
| What | Why we have it | Where it comes from |
|---|---|---|
| Your name, email address and password hash | To give you an account, sign you in, and email you about it. The address is also what a session is signed against. | You, at sign-up |
| Your account, its members and their roles | To decide who may change what the account pays for and who may only run scans. | You, and people you invite |
| The websites you register, their hostnames and site keys | To licence the widget per hostname and to know which site a report belongs to. | You |
| Scan and crawl results: findings, rule ids, the URLs audited, screenshots of the pages, and generated fixes | This is the product. It is also your own evidence of what was found and what changed. | Loading the site you asked us to test |
| Sign-in details for a site behind a login, when you supply them | To audit pages that only exist after signing in. Held encrypted, used for the run, then discarded. | You, deliberately, per crawl |
| Usage counts: scans run, crawls run, widget loads per month | To enforce plan allowances and to bill the right band. | The product, as you use it |
| Billing identifiers from our merchant of record | To know what you are entitled to, and to reconcile what was paid. We never see your card. | Dodo Payments, on a verified webhook |
| Support email and anything you attach to it | To answer you, and to find the thread again when you come back to it. | You |
| Request logs, including IP address, held by our host | Security, abuse and debugging. Not used to build a profile of you. | Your browser, as it reaches us |
We do not ask for special-category data, and there is no field in the product for it. If you put it into a support email or a page we audit, it will be in what we hold, which is a reason to think twice before pasting a production database into a bug report.
What the widget records about your visitors
This is the section most worth reading, because it is the one where someone else’s data is involved and where the industry’s reputation is poor.
A visitor’s choices — larger text, higher contrast, a reading profile — are stored in their own browser, in localStorage, under a single key, per origin. Nothing about those choices is sent to us. Clearing site data or using a private window resets them.
The widget sets no cookie, on your site or ours. The endpoint that receives its telemetry is forbidden from setting one, and there is a test asserting the header is absent from every response it can produce.
What may never be in a beacon payload, as a matter of what the code accepts:
- no cookie, and no identifier for the device or the visitor;
- no IP address stored against the event;
- no user agent, and no assistive-technology signal — we do not detect screen readers;
- no page text and no form values;
- no search query — only its length, because how long a query is tells us whether search is being used and what it says does not;
- no full URL: path-shaped values are redacted in the browser, before transmission, which is the difference between a policy and a control.
An event that is not in the registry below is rejected at the boundary with a 400 rather than accepted and quietly dropped, and so is a key that is not listed against its event. Most events are sampled rather than sent every time; the rate is in the table.
One event, the report that a fix no longer matched its page, can start a re-audit of that page type. So we count it only when the request names your own site, and we count how many different networks sent it rather than how many reports arrived. Each network sets one of 64 marks for that page type that day, chosen by a keyed hash of the network, the site, the page type and the date, so the same network lands on an unrelated mark on another page type, another day or another site. The marks are cleared once their day is over, and the address itself is never stored.
Every beacon the widget may send (62)
| Event | Keys beyond the envelope | Sampled | What it tells us |
|---|---|---|---|
lk_load_success | site_key, event, path, widget_version, ts, duration_ms | 5% of occurrences | How long we take to become available. |
lk_load_error | site_key, event, path, widget_version, ts, reason | every occurrence | Why we did not load. Rare and cheap. |
lk_load_retry | site_key, event, path, widget_version, ts, attempt | every occurrence | Retry pressure on the config endpoint. |
config_fetch_failed | site_key, event, path, widget_version, ts, status, attempt | every occurrence | Our own availability signal. |
duplicate_install | site_key, event, path, widget_version, ts, versions | every occurrence | Two install methods on one page. |
nested_context | site_key, event, path, widget_version, ts, same_origin, depth | 5% of occurrences | How often we load in a frame. |
init_delay_ms | site_key, event, path, widget_version, ts, delay_ms | 5% of occurrences | A consent tool delaying the menu. |
style_blocked | site_key, event, path, widget_version, ts | every occurrence | style-src refused our stylesheet. |
storage_unavailable | site_key, event, path, widget_version, ts, reason | every occurrence | Rare and cheap; drives the honest relabel. |
trigger_collision | site_key, event, path, widget_version, ts, other_selector, other_zindex | every occurrence | Names the colliding widget so the portal can too. |
panel_occluded | site_key, event, path, widget_version, ts, other_selector | every occurrence | The panel opened behind something. |
locale_fallback | site_key, event, path, widget_version, ts, requested, served | 5% of occurrences | Which unshipped tags visitors ask for. |
shortcut_suppressed | site_key, event, path, widget_version, ts, context | 5% of occurrences | Proof in aggregate that the field guard fires. |
adjustment_ineffective | site_key, event, path, widget_version, ts, key | every occurrence | The host overrode a property we own. |
reset_used | site_key, event, path, widget_version, ts, profile_count | every occurrence | Which combinations drive people to reset. |
hidden_state | site_key, event, path, widget_version, ts, reason | 1% of occurrences | How often the widget is hidden. |
lk_launcher_view | site_key, event, path, widget_version, ts | 5% of occurrences | Denominator for launcher clicks. |
lk_launcher_click | site_key, event, path, widget_version, ts, method | every occurrence | Mouse, keyboard or shortcut. |
lk_launcher_render_error | site_key, event, path, widget_version, ts, reason | every occurrence | The launcher failed to mount. |
lk_panel_open | site_key, event, path, widget_version, ts, trigger | every occurrence | Click or shortcut. |
lk_panel_close | site_key, event, path, widget_version, ts, method | every occurrence | Esc, button or outside click. |
lk_profile_toggle | site_key, event, path, widget_version, ts, profile_id, enabled, method | every occurrence | Which profiles are used. |
lk_profile_apply_partial_error | site_key, event, path, widget_version, ts, profile_id | every occurrence | A profile applied incompletely. |
lk_tab_change | site_key, event, path, widget_version, ts, tab | 5% of occurrences | Which categories are browsed. |
lk_adjustment_toggle | site_key, event, path, widget_version, ts, key, enabled | every occurrence | Which adjustments are used. |
lk_adjustment_stepper_change | site_key, event, path, widget_version, ts, key, value | 5% of occurrences | Where people settle on a stepper. |
lk_adjustment_conflict_resolved | site_key, event, path, widget_version, ts, group, selected | 5% of occurrences | Exclusive groups in use. |
lk_adjustment_apply_error | site_key, event, path, widget_version, ts, key | every occurrence | An adjustment threw on this page. |
lk_search_open | site_key, event, path, widget_version, ts | 5% of occurrences | Search discovery rate. |
lk_search_query | site_key, event, path, widget_version, ts, length | 5% of occurrences | Query length only. Never the query. |
lk_search_result_select | site_key, event, path, widget_version, ts, item_id, item_type | 5% of occurrences | What search leads to. |
lk_search_no_results | site_key, event, path, widget_version, ts | 5% of occurrences | How often search dead-ends. |
lk_reset_open | site_key, event, path, widget_version, ts | every occurrence | Reset intent. |
lk_reset_confirm | site_key, event, path, widget_version, ts | every occurrence | Reset completed. |
lk_reset_cancel | site_key, event, path, widget_version, ts | every occurrence | Reset abandoned. |
lk_reset_noop | site_key, event, path, widget_version, ts | every occurrence | Reset with nothing on. |
lk_reset_partial_failure | site_key, event, path, widget_version, ts | every occurrence | A revert threw. |
lk_statement_open | site_key, event, path, widget_version, ts, mode | 5% of occurrences | Drawer or external. |
lk_statement_pending_view | site_key, event, path, widget_version, ts | every occurrence | Statement not generated yet. |
lk_statement_feedback_click | site_key, event, path, widget_version, ts | every occurrence | Feedback path used. |
lk_keyboard_mode_toggle | site_key, event, path, widget_version, ts, enabled | every occurrence | Keyboard mode adoption. |
lk_quicknav_use | site_key, event, path, widget_version, ts, key, target_type | 5% of occurrences | Which quick-nav keys are used. |
lk_quicknav_wrap | site_key, event, path, widget_version, ts, key | 5% of occurrences | Wrapping is announced, not silent. |
lk_reading_mask_toggle | site_key, event, path, widget_version, ts, enabled, variant | every occurrence | Mask or guide. |
lk_reading_mask_height_change | site_key, event, path, widget_version, ts, value | 5% of occurrences | Band heights people choose. |
lk_reading_mask_input_mode | site_key, event, path, widget_version, ts, mode | 5% of occurrences | Mouse, touch or keyboard tracking. |
lk_prefs_persisted | site_key, event, path, widget_version, ts, count_active | 5% of occurrences | How much is typically turned on. |
lk_prefs_storage_blocked | site_key, event, path, widget_version, ts | every occurrence | Storage refused a write. |
lk_prefs_restored_on_load | site_key, event, path, widget_version, ts, count_active | 5% of occurrences | Return-visit restore rate. |
lk_prefs_schema_invalid_dropped | site_key, event, path, widget_version, ts | every occurrence | A stored value failed validation. |
lk_mobile_sheet_open | site_key, event, path, widget_version, ts | 5% of occurrences | Mobile usage. |
lk_mobile_sheet_close | site_key, event, path, widget_version, ts, method | 5% of occurrences | Button, swipe or backdrop. |
lk_mobile_keyboard_adjust | site_key, event, path, widget_version, ts | 5% of occurrences | Virtual keyboard handling fired. |
lk_hide_open | site_key, event, path, widget_version, ts | every occurrence | Hide intent. |
lk_hide_confirm | site_key, event, path, widget_version, ts, scope | every occurrence | 24h or this site. |
lk_hide_cancel | site_key, event, path, widget_version, ts | every occurrence | Hide abandoned. |
lk_hide_expired_restored | site_key, event, path, widget_version, ts | every occurrence | A 24h hide expired on its own. |
lk_footer_link_click | site_key, event, path, widget_version, ts, target_host | 5% of occurrences | Host only, never the full href. |
lk_template_seen | site_key, event, path, widget_version, ts, template_hash | 5% of occurrences | RE-16: the change-detection beacon. |
observer_throttled | site_key, event, path, widget_version, ts, records, duration_ms | every occurrence | The mutation observer degraded. |
lk_manifest_load_error | site_key, event, path, widget_version, ts | every occurrence | Remediation degraded, interface unaffected. |
lk_manifest_guard_miss | site_key, event, path, widget_version, ts, template_hash, tag, ancestors, fingerprint, text, image, absent, bailed | every occurrence | How often a fix no longer matched the element it was made for, as counts per check. Enough of them, from enough different networks on your own site, mark a page type for re-audit. |
Every event also carries the envelope: the site key, the event name, the redacted path, the widget version and a timestamp.
Because none of this is a cookie and none of it identifies a person, installing the widget does not, by itself, give you something to add to a consent banner. That is our reading and not legal advice about your jurisdiction — the cookie policy sets out the facts you would need to make that call.
If you connect a Shopify store
The LatchKey app for Shopify puts the same widget on your storefront through a theme app embed. Installing it gives us a small, fixed set of things about your store, and nothing about the people who buy from it:
- The store’s domains: its myshopify.com address and its primary domain, so the widget’s licence answers on both.
- The permission you granted:
read_themes, which lets us read your published theme’s settings to see whether the app embed is switched on. The site key the embed loads is written to the app’s own data on your store, which needs no further permission. - An access token from Shopify: it expires, it is refreshed only while the app is in use, and it is held encrypted in our secret store. It is never shown to anyone, returned in a response or written to a log.
No customer or order data. The app does not ask for access to your customers, orders or products, and so it cannot read them. When Shopify forwards a shopper’s request to see or erase their data, we have none from Shopify to return or erase, and we record that the request arrived. The notice Shopify sends us when the app is removed carries the store owner’s contact details; we read which store it is and keep nothing else from it.
When you uninstall, the access token is deleted first and the installation is suspended. When Shopify then asks us to erase the store’s data (it sends that request 48 hours after an uninstall), we erase what came from Shopify: the store’s myshopify.com address and the domains the app registered on its licence, the store’s details on our installation record and any token that is left, and we record the request as completed.
If you had connected the store to a LatchKey account, the licence and the website, with their settings and history, belong to that account and stay with it, as the rest of this notice describes. They keep the store’s primary domain as their name, because a licence and a website are named by the domain they serve. We also keep a record that the domain has had its one free trial, which is kept even if the licence is deleted, so that the trial cannot be taken twice. Ask privacy@thelatchkey.in to delete the licence and the website too.
On the storefront, the widget is the one described in what the widget records about your visitors: no cookie, choices kept in the shopper’s own browser, and only the beacons in that table.
If you connect a Wix site
The LatchKey app for Wix puts the same widget on your published site through an embedded script, and shows its status in your Wix dashboard. Installing it gives us a small, fixed set of things about your site, and nothing about its visitors or members:
- The site’s Wix identifier: the id Wix gives the app on your site, and, for a site you duplicated, the id of the site it was copied from, so its settings can start from the original’s.
- The site’s published addresses: its primary domain and its other connected domains, so the widget’s licence answers on them. We read the free Wix address too, and do not keep it: it is shared by every free site on a Wix account.
- The permissions you granted:
SCOPE.DC-SITES.READ-URLS,SCOPE.DC-APPS.MANAGE-EMBEDDED-SCRIPTS,SCOPE.DC.MANAGE-YOUR-APP, which let us read the site’s addresses, add the widget’s script with your site key, and hear when the app is installed or removed. - Who opened the dashboard: Wix signs each visit to the app’s dashboard page, naming the site, whether the visitor is its owner, and Wix’s ids for that person and for the site’s owner. We check the signature and whether it is the owner, and keep none of it.
No Wix token is stored. When we need to call Wix for your site, the app’s own credentials and the site’s id get a short-lived access token, which expires within hours and is kept only in our server’s memory. It is never written to our database, never shown to anyone, never returned in a response and never written to a log.
When you remove the app, Wix tells us and the installation is suspended. Its record, the identifiers and addresses above, is kept for at least 90 days after the removal, so that adding the app back in that time restores everything, and the owner of the LatchKey account the site was connected to is emailed that date. The record is not yet deleted by itself when the date passes: write to privacy@thelatchkey.in and we delete it, before that date or after it. A connected site’s licence and website belong to the account, as they do for a Shopify store above, and stay with it until you ask us to delete them too.
On the published site, the widget is the one described in what the widget records about your visitors, and it does not run in the Wix editor or its preview.
Our legal bases
Where the GDPR or the UK GDPR applies, we rely on:
- Performance of a contract — running your account, scanning the sites you asked us to scan, serving the widget, and taking payment.
- Legitimate interests — security, abuse prevention, debugging, and counting usage against your plan. We have balanced these against your interests; the data involved is minimal and none of it is used to profile anybody.
- Legal obligation — keeping the records tax and accounting law require.
- Consent — only for marketing email, which you can withdraw from any message.
For the sites we audit on your instructions, you are the controller and your legal basis is yours. Our contract with you is the instruction.
Who else touches it
One list, covering everyone. It is generated from the same registry the product uses, so a vendor cannot be introduced without appearing here.
| Who | What for | What reaches them | Role |
|---|---|---|---|
| Dodo Payments | Merchant of record: taking payment, calculating tax, issuing invoices and receipts |
| Independent controller |
| Ollama Cloud | Generating alt text and inferred accessibility fixes |
| Processor |
| Cloudflare R2 | Storing screenshots, generated reports and signed manifests |
| Processor |
| Vercel | Hosting the web application and counting page views on it |
| Processor |
| Resend | Sending the email we send: sign-in, verification, reports and notices |
| Processor |
| Fly.io | Running the browser that loads the site being audited, and the crawler |
| Processor |
Dodo Payments is not merely our processor. Dodo Payments is our merchant of record and the legal seller of your subscription. They take the payment, calculate and remit any tax, and issue your invoice and receipt. They decide their own purposes for the tax and invoicing data they collect as the seller, which makes them an independent controller and their privacy notice the one that governs it.
If you run inference on your own hardware, the inference row disappears from your set entirely: your image bytes never leave your infrastructure, and that is a property of the deployment rather than a promise in a document.
One row is outstanding: the company that hosts the application database. It is set per deployment and this page will name it once it is filled in, which is why the notice at the top of the page lists it.
We will tell customers before adding a sub-processor that changes where their data goes. Ask privacy@thelatchkey.in for a data processing agreement.
Where it goes
Our hosted deployment runs in the United States by default, and data reaches the sub-processors above under Standard Contractual Clauses where a transfer needs a mechanism.
An account can be pinned to the EU, which moves storage of screenshots and artefacts to an EU bucket. Pinning is per account and it is honest about its edges: it applies to the stages that are actually pinned, and the product derives the sentence describing that rather than letting a reader complete “EU region option” for themselves.
Region pinning applies from the date it was enabled. Data captured before then stays where it was written until it is migrated or reaches its retention ceiling.
How long we keep it
| What | How long | Why |
|---|---|---|
| Your account, websites and licences | While the account exists, then removed on request | This is the record of what you asked us to do and what you are entitled to. |
| Scan and crawl results, including findings and fix suggestions | While the account exists, then removed on request | Your own history of what was found and what changed, which is the evidence a report is made of. |
| Screenshots of the pages we audited | Deleted with their scan; orphaned uploads are swept within the hour | A screenshot is a copy of your page, so it lives no longer than the finding that needed it. |
| The crawl frontier: the list of URLs a crawl walked | 30 days after the crawl finishes | Useful while you are reading the report and nothing but a URL list afterwards (CRAWL_FRONTIER_RETENTION_DAYS). |
| Telemetry the widget sends: the events listed above, with no IP address and no cookie | 90 days, then deleted | Long enough to look back over a quarter when a fix misbehaves or a manifest is questioned, and no longer (WIDGET_BEACON_RETENTION_DAYS). |
| Reports that a fix no longer matched its page: counts per check, per page type, per day | While the licence exists. The marks that tell many networks from one are cleared once their day is over | The counts are how a broken fix gets noticed. The marks exist only to tell many visitors from one, which takes no longer than the day they count. |
| Sign-in details you give us for a site behind a login | Held encrypted for the run, then discarded | They exist to open your own site once. Keeping them past the run would be holding a key we no longer need. |
| Payment webhooks and the audit log | Retained, append-only | These are the record that answers "why does this licence believe it is paid?" in a dispute, so nothing in them is edited or deleted. |
| Support email | Two years from the last message in the thread | Long enough to pick up a thread you return to, short enough that it is not an archive of everything anyone ever asked. |
Billing is its own answer, because some of it survives the account on purpose. After an account is erased we keep the identifiers and amounts needed to reconcile what we were paid, and nothing that describes the person:
| Field | Kept? | Why |
|---|---|---|
dodo_customer_id | Kept | Matching Dodo’s payout statements to our own accounts when we file corporate tax |
dodo_subscription_id | Kept | Tying a payout line back to the subscription it came from, without holding the subscriber’s details |
dodo_payment_id | Kept | Identifying a single charge in a dispute or a refund enquiry after the account is gone |
amount and currency | Kept | Reconciling what Dodo paid us against what we recognised as revenue |
issued-at date | Kept | Placing each amount in the right accounting period |
billing address | Not kept | Collected by Dodo at checkout to work out your tax, and held by them as the invoice issuer |
VAT or tax id | Not kept | Validated by Dodo at checkout to apply reverse charge, and held by them on the invoice |
mirrored invoice PDF | Not kept | Never stored by default; a mirror inherits the PII TTL |
Dodo Payments retains its own seller-of-record records, your invoices and receipts, for the statutory tax period. Those are outside what we can erase, and we forward that part of a request to them.
How it is protected
Everything travels over TLS. Passwords are hashed, never stored or recoverable. Sign-in details for a site behind a login are encrypted, held for the run and discarded, and they live in the memory of one machine for the duration of the crawl rather than being written somewhere convenient.
Access to production data is limited to the people who need it, staff actions on customer accounts are written to an append-only audit log, and a staff session expires after twelve hours rather than the thirty days a customer session lasts.
We never log full URLs, credentials or page contents. Report a vulnerability to security@thelatchkey.in; we will acknowledge it and we will not pursue anybody for a good-faith report.
Your rights, and how to use them
Depending on where you live, you can ask for a copy of your data, ask us to correct it, ask us to delete it, ask us to restrict or stop a particular use, object to processing based on legitimate interests, and ask for it in a portable form. You can complain to your data protection authority, and we would rather you told us first.
Write to privacy@thelatchkey.in. We will acknowledge within a few days and answer within 30 days, and we will tell you if we need longer and why.
An erasure request gets a specific answer, not a reassurance. We work through each subsystem in order, and the reply names anything we kept, which subsystem it is in, and the reason — a legal retention period, an active dispute, or a litigation hold. Where the thing you want erased is held by our merchant of record rather than by us, we forward that part of the request to them and tell you we have.
If you are a visitor to a site that runs our widget and you want to exercise a right, ask the site owner: they are the controller, and we act on their instructions. If you cannot reach them, write to us and we will try to put you in touch.
If you are in California or another US state with a privacy law: we do not sell personal information and we do not share it for cross-context behavioural advertising, so there is nothing to opt out of. The rights to know, delete, correct and not be discriminated against are exercised at the same address as everyone else’s.
Automated decisions, and what the models see
Nothing here makes an automated decision with a legal or similarly significant effect on a person. The product decides what to fix on a web page, not anything about you.
Where you have asked for AI-generated fixes, what reaches the inference provider is image bytes and URLs from the site being audited plus the surrounding page text used as context. Your account details are not sent, and the provider does not train on it under our agreement. A self-hosted deployment sends nothing outside your own infrastructure.
Children
This is a product for the people who build and maintain websites. It is not directed at children, we do not knowingly collect data from anyone under 16, and if you think we have, write to privacy@thelatchkey.in and we will delete it.
Changes to this notice
The date at the top of this page is when it last changed. If a change materially affects how we use data we already hold, we will email account owners before it takes effect rather than relying on you to re-read the page.
The other policies: Terms of Service, Privacy Notice, Cookie Policy and our accessibility statement.