Audience collect API
United Pet Club members on the Pro level of Audience Insights can connect their own websites so visits there are counted next to their UPC pages and attributed to the animals, litters and listings the pages are about. This reference is for the developers, agencies and plugin authors who wire a member’s site to the collector.
Base URL: https://audience.unitedpetclub.com (test: https://audience-test.unitedpetclub.com,
staging: https://audience-staging.unitedpetclub.com). The collector is a dedicated public
host, not the API gateway: it needs no partner token, accepts anonymous cross-origin posts from
registered sites, and answers 202 immediately — nothing on it can slow a page down.
Credentials: site key vs site secret
Section titled “Credentials: site key vs site secret”A member registers a site at Dashboard → Insights → External Sites and receives two values:
| Value | Shape | Where it may live | Used by |
|---|---|---|---|
| Site key | pk_… | in the page (data-site, X-Upc-Site, ?k=) — public, printable | the browser script; identifies the site |
| Site secret | sk_… | a server, a plugin’s settings, an environment variable — never a web page | the server collector (Authorization: Bearer sk_…); makes the body’s visitorIp / userAgent trusted |
The secret is shown once at registration; the platform stores only a hash. The member can
rotate it from the same screen (the old one stops working at once). Hits are accepted only while
the site is active — i.e. the member has verified the domain (DNS TXT
upc-audience-verify=<token> or <meta name="upc-audience-verification" content="<token>"> on
the home page) and the site is not suspended.
The browser script (upc.js)
Section titled “The browser script (upc.js)”The simplest integration is one tag on every page:
<script defer src="https://audience.unitedpetclub.com/api/upc.js" data-site="pk_your_site_key"></script>upc.js is dependency-free and posts to {script origin}/api/v1/hits unless data-endpoint
overrides it. On load and on every pushState / popstate navigation it sends one hit carrying
the page URL, title and referrer, the screen and viewport size, device
pixel ratio, colour depth, orientation, touch, colour scheme, reduced-motion preference,
languages, IANA time zone and offset, connection type, and the page facts below. When the page
is hidden or unloaded it sends a follow-up (update: true) with the time the visitor actually
spent (visible, focused, with input in the last 30 s) and the maximum scroll depth, via
navigator.sendBeacon with a fetch keepalive fallback.
Referrer on client-side navigations (version 2). The first hit of a page load sends
document.referrer: the site that sent the visitor. For each later pushState / popstate
navigation, version 2 sends the previous page’s URL as the referrer, because the browser keeps
document.referrer fixed at the first load. So only the page a visitor landed on is credited to
Google, an AI assistant or a campaign, and later page moves count as internal. Version 1 repeated the
first referrer on every navigation of a single-page app, which counted each in-app page as a new
search or social referral. window.__upcAudience.version reports "2".
Caching and pinning a version
Section titled “Caching and pinning a version”| URL | Cache | Use it when |
|---|---|---|
https://audience.unitedpetclub.com/api/upc.{version}.js, e.g. upc.2.js | one year, immutable | Recommended. Browsers fetch the script once and reuse it. When the changelog announces a new version, change the number in your tag. |
https://audience.unitedpetclub.com/api/upc.js | one hour | You cannot edit the tag easily. Visitors pick up a new version within an hour, at the cost of a revalidation per hour. |
The number in the path is only a cache key: every URL serves the current script. Pinning
upc.2.js does not keep you on version 2 once version 3 ships. It means visitors who already have
the file keep using it until their cache expires or you change the number. (Until 2026-09-29 the bare
upc.js was also cached for a year, so browsers could keep a copy that never updated. Pinning a
version avoids that ambiguity.)
<script defer src="https://audience.unitedpetclub.com/api/upc.2.js" data-site="pk_your_site_key"></script>| Attribute / global | Effect |
|---|---|
data-site (or window.__upcAudienceSite) | the site key — required; without it the script does nothing |
data-endpoint | alternative collect URL (default {script origin}/api/v1/hits) |
data-consent="granted" | lets the script set the persistent visitor cookie upc_vid (random, one year, SameSite=Lax, Secure on https). Without it, and always when the browser sends Global Privacy Control, the script is cookieless |
window.__upcAudienceConsent = true | false | the same switch from your consent manager, evaluated at each send |
The script never sends the raw IP (the server sees it), e-mail, names, form values, or query parameters that look like tokens. Returning-visitor accuracy without the cookie relies on a daily rotating hash on the server side; two sites never share a visitor identity.
Page facts: <meta name="upc:subject">
Section titled “Page facts: <meta name="upc:subject">”To attribute a visit to a platform entity, declare the entity on the page. The script reads every
<meta name="upc:subject"> (a KIND:guid, several allowed, comma / semicolon / space separated;
the first is the primary subject) and every data-upc-subject attribute, plus:
<meta name="upc:subject" content="ANIMAL:8a6f1c2e-3b4d-4e5f-9a1b-2c3d4e5f6a7b"><meta name="upc:subject" content="LITTER:0f9e8d7c-6b5a-4f3e-8d2c-1b0a9f8e7d6c"><meta name="upc:page-kind" content="ANIMAL"><meta name="upc:owner" content="<owner profile id>">Subject kinds: PROFILE, ANIMAL, LITTER, LISTING, CONTRACT, RECIPE, BLOG_POST,
ARTICLE, PETLINK_PAGE, PETLINK_NODE, STOREFRONT, OFFER, DESIGN_TEMPLATE,
SHORT_LINK, TAG, EVENT, COURSE, PAGE. Page kinds: HOME, PROFILE, ANIMAL,
LITTER, LISTING_HUB, LISTING, STORE, PRODUCT, BLOG_HUB, BLOG_POST, ARTICLE,
RECIPE, CONTRACT, PETLINK, SEARCH, BREED, SPECIES, STATIC, OTHER. The ids are the
platform ids of the member’s entities (an animal’s id is in its UPC page address). A visit to a
page with no subject still counts for the site; it just does not roll up to an entity.
Actions: upc.track()
Section titled “Actions: upc.track()”Outbound-link clicks and file downloads (pdf, docx, zip, images, media, …) are recorded
automatically. Anything else:
upc.track("CONTACT", { subject: "ANIMAL:8a6f…", code: "email-breeder" });upc.track("PURCHASE", { valueMinor: 12500, currency: "CAD", props: { sku: "PUPPY-KIT" } });kind is one of CLICK, OUTBOUND_CLICK, DOWNLOAD, CONTACT, INQUIRY, SHARE, SAVE,
ADD_TO_CART, PURCHASE, DONATE, SCAN, VIDEO_PLAY, FORM_SUBMIT or CUSTOM; options are
code (max 64), url, subject, valueMinor + currency, and props (small JSON, no personal
data by contract). window.__upcAudience also exposes pageView(), flush() and version.
The collect endpoint
Section titled “The collect endpoint”POST /api/v1/hits — Content-Type: application/json (the script posts text/plain to stay a
simple request), body ≤ 64 KB, ≤ 200 hits per envelope. POST /api/v1/actions is an alias for
action-only envelopes. The site key travels in X-Upc-Site or ?k=, or as site in the body.
{ "site": "pk_your_site_key", "sentAtUnixMs": 1788300000000, "hits": [ { "hitId": "019a3f1e-7c2b-7d4e-8a9b-0c1d2e3f4a5b", "url": "https://www.example.com/puppies/luna", "host": "www.example.com", "path": "/puppies/luna", "title": "Luna — available", "referrer": "https://www.google.com/", "occurredAtUtc": "2026-09-06T14:03:11Z", "collector": "SERVER", "visitorIp": "203.0.113.42", "userAgent": "Mozilla/5.0 …", "acceptLanguage": "en-CA,en;q=0.9", "subjects": ["ANIMAL:8a6f1c2e-3b4d-4e5f-9a1b-2c3d4e5f6a7b"], "pageKind": "ANIMAL" } ], "actions": []}Hit fields
Section titled “Hit fields”| Field | Type | Notes |
|---|---|---|
hitId | UUID (v7 preferred) | client-minted; a browser hit that reuses a server hit’s id merges into it (one visit, both sets of facts). Otherwise two hits for the same visitor and path within 10 s merge on their own |
url, host, path, locale, title, referrer | string | query strings are dropped except for UTM / click-id parameters |
occurredAtUtc | ISO 8601 | re-based on the server clock using sentAtUnixMs |
collector | EDGE | BROWSER | SERVER | who produced this input |
visitorIp, userAgent, acceptLanguage, clientHints | string / object | trusted only from the server collector (bearer secret); a browser post carrying them is ignored |
signedIn, profileId, profileKindCode, professionalKindCodes, accountKindCode | server-only; a signed-in UPC member on your site (rare outside UPC-hosted sites) | |
httpStatus, prerender | server-only; speculative loads are counted separately, never as visits | |
visitorId | string | the upc_vid value, when consented |
screen { w, h, dpr, colorDepth, orientation }, viewport { w, h }, touch, colorScheme, reducedMotion, languages[], timeZone, utcOffsetMinutes, connection | browser facts | |
durationMs, activeMs, scrollDepthPct, update | engagement; update: true marks a follow-up for an earlier hitId | |
subjects[], pageKind, ownerProfileId | what the page is about; first subject is primary |
verifiedCrawler and internal exist on the wire but are honoured only from the platform’s own
edge collector — a partner cannot mark a hit as a verified crawler.
Action fields
Section titled “Action fields”hitId, kind, code, targetUrl, subject, valueMinor, currencyCode, props,
occurredAtUtc, and (server collector only) visitorIp, userAgent, visitorId, profileId.
Trust, Origin and CORS
Section titled “Trust, Origin and CORS”The collector decides how much of the body to believe from the credential it sees:
| Credential | Collector | Body fields believed |
|---|---|---|
Authorization: Bearer sk_… matching the site | SERVER | everything except verifiedCrawler / internal |
| site key only | BROWSER | page and device facts; IP, UA and profile fields are stripped. The request’s Origin (or Referer) host must be one of the site’s registered domains — a post from any other origin answers 403 as an invalid site key. Add every host you serve from (example.com and www.example.com are different) under Extra domains |
CORS is open for the collect routes: the response echoes the request Origin
(Access-Control-Allow-Origin: <origin>, Vary: Origin), allows POST, GET, OPTIONS and the
headers Content-Type, X-Upc-Site, X-Upc-Hit, Authorization, and caches the preflight for a day.
OPTIONS answers 204.
Responses
Section titled “Responses”202 Accepted — { "data": { "accepted": 1, "dropped": 0, "sampled": false }, "resultCode": "Audience_AudienceCollect_AD_Hit_SU" }.
sampled: true means the site is under a flood valve and this batch was dropped by sampling;
kept batches are re-weighted, so totals stay approximately right.
| Status | resultCode | Meaning |
|---|---|---|
400 | Audience_AudienceCollect_VL_Request_RJ | malformed envelope |
401 | Audience_AudienceCollect_VL_SiteSecret_RJ | bearer secret does not match |
403 | Audience_AudienceCollect_VL_SiteKey_RJ | unknown key, site not active (unverified), or the browser Origin is not a registered domain |
403 | Audience_AudienceCollect_VL_Site_RJ | site suspended by the platform |
413 | Audience_AudienceCollect_VL_Envelope_RJ | over 64 KB |
429 | Audience_AudienceCollect_HD_Rate_RJ | over 100 hits/s (burst 1,000) for this site — retry with back-off |
503 | Audience_AudienceCollect_HD_Disabled_FL | collection switched off platform-wide — retry later |
GET /api/v1/sites/{siteKey}/ping → { "site": "pk_…", "ok": true, "collectEnabled": true } for
a plugin’s settings screen.
Server-side integration (plugins, hosted sites)
Section titled “Server-side integration (plugins, hosted sites)”Send a server hit from the request that renders the page — for WordPress, template_redirect —
with the visitor’s IP and User-Agent, and put the same hitId in a <meta name="upc:hit"> tag so
the browser script’s hit merges into it. That combination counts cached pages, crawlers and
JavaScript-off readers (the server hit) and durations, screens and scroll (the browser hit)
as one visit. Rate limit on your side to well under 100 hits/s per site.
Page registration. On UPC’s own sites every page tells the platform what it is about at
render time; on your site the platform learns it from the upc:subject meta tags the script
sends and from the subjects / pageKind / ownerProfileId you put on server hits. Declare
the facts on every hit rather than once — a hit that arrives before the page is known is
re-attributed within 15 minutes once it is, but a page nobody declared stays a plain site visit.
The plugin-config read the member sees (GET …/me/sites/{id}/plugin-config) renders exactly
this: the site key, the collect URL, the script tag, the verification record and a subject-meta
example.
Owner read API (not yet available)
Section titled “Owner read API (not yet available)”Reading a member’s insights programmatically — a summary and the visit log per subject for a
member’s own custom website — is planned behind the AUDIENCE_API right, which Audience Insights Pro includes (the yearly AUDIENCE_PRO
add-on; see Platform prices). The token
model is still to be decided, so nothing is exposed to partners today; the tier-gated reads
(/api/audience/v1/me/...) exist only for the signed-in United Pet Club apps. Watch the
Changelog.