# Yaap documentation Public guides for https://yaap.sh. Customer analytics requires scoped credentials. # Collection controls > Choose identified, anonymous or paused collection. Source: https://yaap.sh/docs/collection-controls Markdown: https://yaap.sh/docs/collection-controls.md ## Choose an installation mode \[#choose-an-installation-mode] **Settings → Installation** offers three snippet choices. Changing the selector changes the example code; publish the new snippet on your website to apply it. | Mode | Behavior | | ------------------- | --------------------------------------------------------------------------- | | Full analytics | Collects events with browser and session identifiers. | | Anonymous analytics | Collects events without stored visitor/session identifiers. | | Wait for consent | Starts paused, with identifiers disabled. Your site decides when to resume. | Enabling tracking does not record visitor consent. Connect these controls to your site's consent manager and restore the selected behavior on every page load. ## Start paused \[#start-paused] ```html ``` For npm, use the equivalent initialization options: ```ts const analytics = init({ siteId: "YOUR_SITE_ID", host: "https://analytics.example.com", identifiers: false, tracking: "paused", }); ``` Import `init` from `@yaap/client` as shown in the [npm guide](https://yaap.sh/docs/npm). ## Apply a visitor's choice \[#apply-a-visitors-choice] After the tracker has loaded, use these calls from your consent manager's callback. For npm, you can use the returned tracker instance instead of `window.osAnalytics`. ```js title="Allow identified analytics" window.osAnalytics?.setIdentifiers(true); window.osAnalytics?.resume(); ``` ```js title="Allow anonymous analytics" window.osAnalytics?.setIdentifiers(false); window.osAnalytics?.resume(); ``` ```js title="Stop collection and remove stored identifiers" window.osAnalytics?.pause(); window.osAnalytics?.setIdentifiers(false); ``` `pause()` aborts pending requests and stops collection. `resume()` records the current page without replaying activity that happened while paused. Disabling identifiers removes stored identifiers while allowing anonymous collection if tracking is running. If the visitor makes a choice before the script loads, retain that choice in your consent manager and apply it once the tracker is available. Optional chaining alone does not queue a choice. ## What anonymous data can show \[#what-anonymous-data-can-show] Anonymous events contribute to event counts and goal completions. Reports that require identified visitors or sessions, including ordered funnels, cannot reconstruct those identities from anonymous events. --- # Custom events > Record meaningful actions with typed properties. Source: https://yaap.sh/docs/events Markdown: https://yaap.sh/docs/events.md ## Send an event \[#send-an-event] After the tracker loads, record an action when it actually happens: ```js window.osAnalytics?.track("signup", { plan: "pro", seats: 3, trial: false, }); ``` With npm, call `track()` on the instance returned by `init()`: ```ts const accepted = await analytics?.track("download", { asset: "getting-started.pdf", }); ``` The promise resolves to whether the server accepted the event. Acceptance does not mean a queued event is already visible in reports. Invalid events resolve to `false`. ## Name events consistently \[#name-events-consistently] Event names are case-sensitive and contain 1–64 letters, digits, underscores, dots or hyphens. `pageview` is reserved for automatic page tracking. Use a stable name such as `signup` and put variations in properties. This keeps filtering and goal definitions consistent as your product changes. ## Property limits \[#property-limits] * Up to 20 properties per event, within a 2 KiB JSON payload. * Values are strings, finite numbers or booleans; nested objects and arrays are not supported. * Keys begin with a letter and contain up to 64 letters, digits or underscores. * Strings are limited to 256 characters and cannot contain control characters. Keep property types consistent: `3`, `"3"` and `true` are different values. Send the product context you need for reports without including passwords, credentials or payment-card details. ## Find the event \[#find-the-event] Open **Events** on the same website, choose a date range that includes your test, and filter by the event name or its properties. Confirm your website's reporting timezone under **Settings → General**. Then [create a goal](https://yaap.sh/docs/goals-and-funnels) matching the event to measure conversion. --- # Goals and funnels > Measure completed actions and the steps leading to them. Source: https://yaap.sh/docs/goals-and-funnels Markdown: https://yaap.sh/docs/goals-and-funnels.md ## Create a goal \[#create-a-goal] Open **Overview → Manage goals**. Match either a custom event name, such as `signup`, or an exact page path, such as `/thanks`. Add up to three property conditions to narrow the goal. A signup goal with `plan = "pro"` only matches events containing that exact value. Every condition must match the same event. Matching is exact and case-sensitive. `/thanks` differs from `/thanks/`. Missing properties do not match, and a number differs from the same value sent as text. ## Read conversion counts \[#read-conversion-counts] **Goal completions** count every matching event, including repeats and anonymous events. **Converted sessions** count distinct identified sessions containing a matching event. Session conversion divides converted sessions by identified sessions with activity matching the report range and dimensions. Anonymous completions do not add identified sessions; without identified sessions, the rate is unavailable. ## Build a funnel \[#build-a-funnel] Open **Funnels** and define the ordered page or event steps your visitors should take. Choose the identity scope and completion window to match the journey you want to measure. Each step needs a later event. A single event cannot satisfy two repeated steps. Funnel conversion divides completions by entrants, and depends on identified activity. For example, track a product journey from a pricing pageview, to a `signup` event, to an `activation` event. Instrument and verify those events before relying on the funnel. ## Understand historical changes \[#understand-historical-changes] Editing a definition recalculates retained history against the new conditions. Page- and property-based goals and funnels depend on retained raw events; they cannot recover matching activity after that history is deleted. Review **Settings → Data retention** before choosing how much history to keep. --- # Get started > Connect your website to Yaap and see your first pageview. Source: https://yaap.sh/docs Markdown: https://yaap.sh/docs.md Yaap brings traffic, custom events, goals, funnels and revenue into one workspace. Start by connecting a website, then add the events that matter to your product. ## 1. Open your workspace \[#1-open-your-workspace] Open the [dashboard](https://yaap.sh/app) and sign in. If you are using Yaap hosted, use the account you registered with the service. For your own installation, follow [Self-hosting](https://yaap.sh/docs/self-hosting) and create the owner account at `/setup` first. ## 2. Add a website \[#2-add-a-website] Add your website with its exact origin, such as `https://example.com`. The scheme, hostname and port must match the website your visitors use. `https://www.example.com` is a different origin; add it under **Settings → Domains & exclusions** if you use both. Your website gets a public site ID. This identifies the destination for analytics; it is not an API secret. ## 3. Install the tracker \[#3-install-the-tracker] Open **Settings → Installation** and copy the snippet generated for your website. It includes your site ID and the correct Yaap server URL. Add it to the shared HTML layout so it loads on every page you want to measure. ```html title="Your website's HTML" ``` Replace the example server and site ID with your own values. If you prefer a package, follow the [npm and React guide](https://yaap.sh/docs/npm). Choose **Full analytics**, **Anonymous analytics**, or **Wait for consent** before copying the snippet. See [Collection controls](https://yaap.sh/docs/collection-controls) for what each option collects. ## 4. Verify your first pageview \[#4-verify-your-first-pageview] Visit your website in a regular browser, then open its **Overview** in Yaap. Collection is processed through a queue, so allow a short delay before refreshing the report. If nothing appears, check that `/script.js` loads and `/ingest` requests reach your Yaap server in the browser's Network panel. Follow the [troubleshooting checklist](https://yaap.sh/docs/troubleshooting) if the pageview is still missing. ## Next steps \[#next-steps] * [Track custom events](https://yaap.sh/docs/events) such as signups and downloads. * [Create goals and funnels](https://yaap.sh/docs/goals-and-funnels) to measure conversion. * [Connect integrations](https://yaap.sh/docs/integrations) for revenue, API access and MCP. --- # Install the tracking script > Add Yaap to a website without a package manager. Source: https://yaap.sh/docs/installation Markdown: https://yaap.sh/docs/installation.md ## Copy your website's snippet \[#copy-your-websites-snippet] Open your website in Yaap and go to **Settings → Installation → Script**. Copy the generated snippet into the shared HTML layout, usually inside ``. Publish the change to your website. ```html ``` The script sends events to the server it was loaded from. Use your own Yaap server URL for self-hosted installations. Do not move the script to a different CDN without configuring how events reach Yaap; use the [npm client](https://yaap.sh/docs/npm) with an explicit `host` for that setup. ## Choose where tracking runs \[#choose-where-tracking-runs] Place the snippet once in the layout shared by the pages you want to measure. The tracker records the initial pageview and observes pathname changes in single-page applications. Query-string and fragment changes do not produce extra pageviews. Install either the script or the npm client. Repeated initialization reuses the first tracker and keeps its original options. ## WordPress and Shopify \[#wordpress-and-shopify] **Settings → Installation** includes WordPress and Shopify instructions. Use the generated snippet for your site and place it in the platform's shared theme or supported custom-code area. Confirm it appears on the published pages you intend to track; theme previews can use a different origin. ## Allow your domains \[#allow-your-domains] In **Settings → Domains & exclusions**, add each origin you use, including `www` or other subdomains. Origins match exactly. Production origins use HTTPS; local development addresses can use HTTP. Use hostname or path exclusions for areas you do not want to collect. `/admin/*` matches `/admin/users` but not `/admin`; add both if needed. Exclusions affect new collection and leave existing history intact. ## Verify installation \[#verify-installation] Visit a published page in a regular browser and check **Overview**. If collection is paused, apply the visitor's choice before expecting events. See [Collection controls](https://yaap.sh/docs/collection-controls) and [Troubleshooting](https://yaap.sh/docs/troubleshooting). --- # API, MCP and revenue > Connect Yaap to your tools and payment workflow. Source: https://yaap.sh/docs/integrations Markdown: https://yaap.sh/docs/integrations.md ## API access \[#api-access] Yaap exposes a scoped public API for reporting and management. Create a credential with access only to the websites and operations the integration needs. Keep secret credentials on your server. Use the [API and MCP reference](https://yaap.sh/docs/api.md) for authentication, scopes, endpoints, pagination, retries and error handling. The live [OpenAPI document](https://yaap.sh/api/v1/openapi.json) describes this installation's API. The public site ID used in the browser tracker is separate from an API credential. ## MCP connections \[#mcp-connections] The MCP endpoint is `/mcp` on your Yaap installation. Follow the authentication and connection instructions in the [MCP reference](https://yaap.sh/docs/api.md). Access depends on the credential's scopes and website grants; connecting a client does not grant it access to every website. ## Revenue \[#revenue] Open **Settings → Revenue** for your website to inspect connection status and configure payment ingestion. Payment provider webhooks and the server payment API support revenue reporting and refunds. Use the [payment integration guide](https://github.com/dagurleo/yaap/blob/main/docs/PAYMENTS.md) for the current provider setup, signature verification, identifiers and attribution fields. Payment ingestion belongs in your server or provider webhook workflow. When validating a connection, keep test and live payments separate and inspect currencies separately in reports. Browser custom events alone do not create a verified payment record. --- # npm and React > Initialize the typed browser client in your application. Source: https://yaap.sh/docs/npm Markdown: https://yaap.sh/docs/npm.md ## Install the package \[#install-the-package] ```sh npm install @yaap/client ``` Copy the initialization code from **Settings → Installation → npm** to get your site ID and server URL. ```ts import { init } from "@yaap/client"; const analytics = init({ siteId: "YOUR_SITE_ID", host: "https://analytics.example.com", }); await analytics?.track("signup", { plan: "pro" }); ``` `host` is your Yaap server, not the website being tracked. Without it, the client sends events to `https://yaap.sh/ingest`. Use an explicit host for self-hosting and local development. ## React \[#react] Initialize in a top-level browser lifecycle and release the tracker on teardown. Mount this component once in your shared application layout. ```tsx title="Analytics.tsx" import { useEffect } from "react"; import { init } from "@yaap/client"; export function Analytics() { useEffect(() => { const analytics = init({ siteId: "YOUR_SITE_ID", host: "https://analytics.example.com", }); return () => analytics?.destroy(); }, []); return null; } ``` For frameworks with server and client component boundaries, put this component in a client module. Imports are safe during server rendering, but `init()` returns `undefined` outside a browser. Optional chaining handles that case. ## Automatic pageviews \[#automatic-pageviews] Initialization sends a pageview and tracks pathname changes automatically. You do not need a separate pageview call on each router navigation. Changes to query strings or fragments do not create additional pageviews. One tracker is supported per page. Repeated calls return the existing instance, including an instance installed by the script tag, and keep the first initialization's options. ## Cleanup and collection choices \[#cleanup-and-collection-choices] `destroy()` stops collection and removes timers, listeners and owned history hooks. Stored identifiers remain for later initialization. To wait for a visitor's choice, initialize with `tracking: "paused"` and `identifiers: false`. Follow [Collection controls](https://yaap.sh/docs/collection-controls) before resuming. --- # Self-hosting > Run Yaap in your own Cloudflare account. Source: https://yaap.sh/docs/self-hosting Markdown: https://yaap.sh/docs/self-hosting.md ## What you deploy \[#what-you-deploy] The default installation uses one Cloudflare Worker, D1, an event queue and a dead-letter queue. PostgreSQL through Hyperdrive is an optional database backend. The app, dashboard, tracking script and these docs ship together. Start from the complete repository, including `apps/web`, `packages/client` and the root lockfile. ## Try it locally \[#try-it-locally] Use Node.js 22.12 or newer. From the repository root: ```sh npm ci npm run db:setup:local npm run db:migrate:local npm run dev ``` Open `http://localhost:8790/app`. On a fresh installation, use the `BOOTSTRAP_SECRET` from `apps/web/.dev.vars` to create the owner account. Add a website and install its tracker with `host: "http://localhost:8790"` for npm, or copy the generated script snippet. ## Deploy to Cloudflare \[#deploy-to-cloudflare] Follow the [deployment guide](https://github.com/dagurleo/yaap/blob/main/docs/DEPLOYMENT.md) for resource provisioning and deployment configuration. 1. Import the full repository into Workers Builds with root directory `/`. 2. Provision D1 and both queues, and configure their identifiers in the root `wrangler.jsonc`. 3. Generate separate `BETTER_AUTH_SECRET` and `BOOTSTRAP_SECRET` values with `openssl rand -hex 32`. Set them as Worker runtime secrets. 4. Use `npm run build` as the build command and `npm run deploy` as the deploy command. 5. Open `/setup` at your deployed URL, create the owner and add a website. 6. Verify `/health`, sign-in and a real pageview from the tracked website. Keep the authentication secret stable across upgrades. The deployment guide records remaining live acceptance checks and the deploy-button prerequisites. ## Database and email options \[#database-and-email-options] The default D1 installation does not require an external database. For PostgreSQL or production Hyperdrive, follow the [database guide](https://github.com/dagurleo/yaap/blob/main/docs/DATABASES.md). Outbound email is optional and is not included in the default production template. Configure it before relying on invitation or other email flows; follow the email setup linked from the deployment guide. ## Upgrade an installation \[#upgrade-an-installation] Back up your database and record your resource IDs, configuration and deployed commit. Keep the same database, queues and secrets, merge the new code, then rebuild and deploy using the normal commands. The deployment script applies migrations for the configured provider. Rolling back Worker code does not undo database migrations; verify your database restore process separately. --- # Troubleshooting > Find out why a pageview or event is missing. Source: https://yaap.sh/docs/troubleshooting Markdown: https://yaap.sh/docs/troubleshooting.md ## No pageviews appear \[#no-pageviews-appear] Work through these checks in order: 1. **Published code:** confirm the tracker is present on the live website, not just in a local editor or theme preview. 2. **Script and destination:** in the browser's Network panel, confirm `/script.js` loads and requests to `/ingest` use the correct Yaap server. 3. **Site ID:** copy it again from the same Yaap installation receiving your events. 4. **Allowed origin:** compare the website's exact scheme, hostname and port with **Settings → Domains & exclusions**. Add `www` and other origins explicitly. 5. **Collection mode:** if initialized paused, apply the visitor's choice and resume. Check hostname and path exclusions too. 6. **Browser:** try a regular browser session. The tracker does not initialize under browser automation, and browser extensions can block requests. 7. **Report range:** check the selected website, date range, timezone and filters. Allow a short delay for queued events. ## The script loads but events are rejected \[#the-script-loads-but-events-are-rejected] Inspect the `/ingest` response in the browser's Network panel. Check the site ID, allowed origin and event payload. A successful script download only verifies that the asset is reachable. For custom events, compare the name and properties with the [event limits](https://yaap.sh/docs/events). If using npm, await `track()` to inspect the acceptance result. ## Self-hosted events are accepted but reports stay empty \[#self-hosted-events-are-accepted-but-reports-stay-empty] Open **Settings → Ingestion** to inspect counters and activity. Check `/health` and verify the event queue producer, consumer and dead-letter queue refer to the intended resources. Confirm all database migrations for your chosen backend have been applied. Follow the [operations guide](https://github.com/dagurleo/yaap/blob/main/docs/OPERATIONS.md) for queue failures, retention and release verification. ## A funnel has fewer visitors than event counts \[#a-funnel-has-fewer-visitors-than-event-counts] Events count occurrences; funnels depend on identified visitors or sessions completing ordered steps within the selected time window. Anonymous events, repeated actions and incomplete journeys can produce a higher event count. See [Goals and funnels](https://yaap.sh/docs/goals-and-funnels). ## Get help \[#get-help] Use the [contact page](https://yaap.sh/contact) or [GitHub issues](https://github.com/dagurleo/yaap/issues). Include the affected guide, installation method, app version and a redacted error response. Remove credentials and private analytics before sharing logs. --- Source: https://yaap.sh/docs/api.md # Public API and MCP YAAP exposes authorized analytics and management at `/api/v1` and remote MCP at `/mcp`. Both transports use the same operation registry, validation, scopes and services. Existing dashboard and collection endpoints remain compatible. ## Enable locally or on your next deployment Apply migration **0018\_public\_api** for D1 or **0006\_public\_api** for PostgreSQL before running the new build. Use the normal migration command for your selected provider; do not switch providers to activate this feature. Existing rows get an initial revision and retain their history. Open **API & MCP access** from the website directory or account menu (`/app/access`). Create a personal token with a name, expiry, action scopes and selected websites. Copy it into your integration's secret settings. Tokens are not displayed again in the list; the server stores a hash. Mutations that return secrets keep an encrypted response for the 24-hour idempotency window. Revocation is immediate for subsequent requests. `BETTER_AUTH_SECRET` also protects API cursors and encrypted retry responses. Keep it stable. No additional infrastructure binding or hosted service is required. ## REST Use `Authorization: Bearer `. OAuth MCP access tokens are audience-bound to `/mcp` and are not accepted by REST. Owner-session access is reserved for credential administration; ordinary public endpoints require a token. The complete machine-readable contract is `GET /api/v1/openapi.json`. `GET /api/v1/capabilities` describes the supported operations, metrics, filters, limits and this credential's scopes. `GET /api/v1/me` returns its grants. ```sh curl "$YAAP_URL/api/v1/sites" \ -H "Authorization: Bearer $YAAP_TOKEN" curl "$YAAP_URL/api/v1/sites/$SITE_ID/reports/overview?from=2026-09-01&to=2026-09-07&compare=previous_period" \ -H "Authorization: Bearer $YAAP_TOKEN" ``` Reads return `{data, meta, pagination?}`. `meta` contains requestId, API/metric versions, an asOf timestamp and report-specific period information. Resource reads include an ETag and `meta.revision`. Lists default to 50 records and allow at most 100; journeys return 100. Use the returned opaque cursor with the same query to read the next page. Cursors expire after 24 hours. Their cutoff stabilizes traversal, but retention and mutable records mean they are not database snapshots. Dates are required for historical queries: inclusive `from` and `to` in the site reporting timezone, up to 366 days ending today or earlier. The current day ends at query time. `compare=previous_period` uses the preceding equal calendar range; a partial current day compares to a full previous period. Live reports reject historical dates. Dimension filters combine with AND. `unknown=country,source` selects unknown values without relying on internal sentinel strings. A dimension cannot have both an exact filter and an unknown filter. `propertyValue` in a REST query is a JSON scalar: `1`, `true`, or `"pro"` including quotes for strings. URL-encode it. MCP accepts the native scalar. Numbers, strings and booleans remain distinct. Unknown fields and unsupported filter combinations are rejected. Pageview breakdowns support all advertised traffic dimensions. Browser-count breakdowns support country/region/city/browser/OS/device. Timeseries currently supports daily pageviews. Breakdown rows are bounded and explicitly report truncation. Geography retains parent dimensions; campaigns retain source/medium/campaign. `value0`, `value1`, `value2` follow that parent-to-child order; `value` is the requested count. The OpenAPI and MCP output schemas specify these fields. Rates are fractions (0–1), unavailable values are null, and session duration is in seconds. Money uses integer minor units with separate currency rows. Revenue uses durable per-payment attribution with a captured model/window. Pending records reconcile for at least 72 hours; finalized records survive raw-event expiry. Payment reads include attribution status and unmatched reasons. See [metric definitions](https://github.com/dagurleo/yaap/blob/main/docs/REPORTING.md$4) and [payments](https://github.com/dagurleo/yaap/blob/main/docs/PAYMENTS.md$4). ## Management and retries Create sites, goals and funnels with POST. PATCH preserves omitted fields; explicit empty `conditions` clears goal conditions. To change a page goal into an event goal, supply `path: null` and an `eventName`. Archive/restore goals and funnels using `archived: true` / `false`. No hard-delete methods are provided for those definitions or websites. All creates and payment-key rotations require `Idempotency-Key`. Reuse it when retrying the same operation and payload. For 24 hours, retries return the original result; changing input under that key returns 409. Secret-setting PUT also requires an idempotency key. PATCH, integration PUT/DELETE, and payment-key rotation require `If-Match` containing the quoted ETag from a fresh read. Missing preconditions return 428; a stale revision returns 412. Refetch and review the new state before applying an edit. A resource mutation, its audit entry, and retry response commit atomically on both databases. Site details, tracking rules and retention share a site revision; editing any of them advances it. Existing dashboard writes also advance revisions. ```sh curl "$YAAP_URL/api/v1/sites/$SITE_ID/goals" \ -X POST -H "Authorization: Bearer $YAAP_TOKEN" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: signup-goal-001' \ --data '{"name":"Pro signup","eventName":"signup","conditions":{"plan":"pro"}}' ``` Collection changes affect future ingestion. Generating a full/anonymous/paused snippet does not deploy it or change a tracker already running on a website. Retention changes can delete expired history during later cleanup; increasing retention cannot restore deleted rows. Stripe configuration and payment ingestion keys are separate from public credentials. Analytics payment methods do not charge cards or execute refunds. ## Scopes Scopes and site grants are both required. `sites:create` is installation-level; newly created sites are added atomically to the calling credential's explicit grants. Other credentials do not automatically gain those sites unless the owner chose all current/future sites. | Permission family | Access | | -------------------------------------------- | ------------------------------------------------------------------- | | `sites:read`, `sites:create`, `sites:write` | Website discovery, creation, details | | `reports:read` | Aggregate traffic, audience, sessions, events, goals, funnels, live | | `events:read` | Event/property discovery and individual event payloads | | `visitors:read` | Browser IDs, visitor lists and journeys | | `goals:read/write`, `funnels:read/write` | Definition discovery and management (separate read/write scopes) | | `revenue:read`, `payments:read` | Revenue aggregates or individual payment details | | `settings:read/write`, `retention:write` | Installation/rules or retention mutation | | `integrations:read/write`, `operations:read` | Integration status/configuration or ingestion diagnostics | | `audit:read` | Management history limited to authorized sites | Event/payment details omit browser/session IDs without `visitors:read`. Journeys omit payments without `payments:read`. Live aggregates omit visitor rows without `visitors:read`. Aggregate reporting never includes raw event/payment lists. Credential and OAuth client administration require the owner session and same-origin writes. ## MCP Configure the full installation URL plus `/mcp`. The server uses **@modelcontextprotocol/sdk 1.30.0**, with **2025-11-25** as its primary protocol revision. Streamable HTTP uses stateless requests and JSON responses. Standalone SSE, subscriptions and protocol sessions are not enabled. The catalog contains 47 business tools for a credential with all scopes, and omits tools it cannot use. Every call checks authorization again. For a client that supports configured HTTP headers, use the personal token as its bearer token. Never place tokens in the URL or tool arguments. Some clients support OAuth only; do not assume a header-based configuration works everywhere. For OAuth, register a client at `/app/access` with the exact redirect URI provided by that client. Copy the client ID into the client's configuration. This release supports **owner-preregistered public clients** using S256 PKCE; it does not implement dynamic client registration or client-ID metadata document fetching. The consent page shows requested scopes and requires a website selection or explicit all-sites choice. | OAuth endpoint | Purpose | | ------------------------------------------------------- | ------------------------------------------------- | | `GET /.well-known/oauth-protected-resource/mcp` | Resource/authorization discovery | | `GET /.well-known/oauth-authorization-server` | Authorization-server metadata | | `GET/POST /oauth/authorize` | Owner login and consent | | `POST /oauth/token` | Single-use authorization code exchange or refresh | | `POST /oauth/revoke` | Token/grant revocation | | `GET/POST /oauth/clients`, `DELETE /oauth/clients/{id}` | Owner-only client registration and removal | Authorization requests must specify the installation's `/mcp` resource, scopes, response\_type=code, registered redirect\_uri, code\_challenge and code\_challenge\_method=S256. Token/refresh requests also specify that resource. Tokens last one hour. Request `offline_access` for rotating refresh tokens with a 30-day maximum grant lifetime. Refresh cannot increase scopes; reuse of a consumed refresh token revokes the grant. Removing a client invalidates its credentials. MCP writes require `revision` (the unquoted `meta.revision`) and creates require `idempotencyKey`. `set_goal_archived` and `set_funnel_archived` provide explicit archive/restore tools. Secret-setting/returning integration operations are REST/owner-settings only. Tool outputs contain structured data and the same JSON as text; tool errors have `isError: true` and a stable error code. Data inside event names, properties and paths is untrusted content, never instructions. Compatibility checked locally with the official TypeScript MCP client against the built Worker: connection, tool discovery, schema validation, reads, writes and scoped visibility. The OAuth code/refresh lifecycle is exercised through HTTP and its access token is used with that SDK. Product-specific desktop/browser connectors and live Cloudflare deployment have not yet been verified. Browser-origin MCP connections currently allow the installation origin only; native/server clients can omit Origin. ### Testing with MCP Inspector Run `npx @modelcontextprotocol/inspector@2.6.0` and open the session URL printed by the launcher. In **Add Servers → Add manually**, choose `streamable-http` and enter `http://localhost:8790/mcp` (adjust the port for your dev server). Create a short-lived personal token at `/app/access`, restricted to a test website. For reporting and goal-management checks, select `sites:read`, `reports:read`, `goals:read`, and `goals:write`. In the Inspector server's **Settings → Custom Headers**, add `Authorization` with value `Bearer `. Keep **Protocol Era** set to **Legacy (2025-11-25 handshake)**; YAAP does not yet implement the modern 2026-07-28 protocol. Connect, list tools, and start with `get_access`, `list_sites`, and `get_overview` using an authorized `siteId` and explicit `from`/`to` dates. Only tools permitted by the token appear. The documented `--server-url http://localhost:8790/mcp --transport http` launch creates a read-only server configuration in Inspector 2.6.0. Launch without a target when you want to configure authentication interactively. An unauthenticated connection fails with `MCP auth challenge (401)`; this is expected. The Inspector's session token authenticates the browser to Inspector and is separate from the YAAP bearer token. See the [official Inspector instructions](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector#remote-http-server). Revoke the YAAP test token when finished. Verified in Inspector 2.6.0 against the local PostgreSQL-backed server on 2026-09-11: Streamable HTTP handshake negotiated 2025-11-25; the four scopes above exposed 18 tools. `get_access` and `list_sites` confirmed the Localhost-only grant. `get_overview` returned valid structured output with zero traffic for September 10–11. `create_goal` succeeded and an identical idempotency-key retry returned the same goal. `set_goal_archived` archived that test goal; a restore attempt with its stale revision returned `revision_conflict`, and `get_goal` confirmed it remained archived. The dedicated test goal is retained as an archived definition. Inspector reports no schema portability errors for these tools, but warns about unconstrained `additionalProperties` schemas in outputs (4 warnings for access/goal tools, 12 for overview). These warnings did not prevent the tested calls; compatibility with stricter clients remains a follow-up. This Inspector check used a personal bearer token, not OAuth. ## Limits and operations Initial limits are conservative defaults, not published capacity benchmarks: 120 ordinary calls and 30 expensive calls per minute per credential and per website; four concurrent calls per credential. Limits return 429 and REST includes Retry-After. Raw event/report complexity is bounded by validated dates, properties, steps and row limits. PostgreSQL retains existing per-statement timeouts. A hard end-to-end request deadline and production capacity measurements remain follow-up hardening. API credentials, clients, and the redacted audit log persist. The hourly cleanup removes expired retry records, OAuth codes and consumed-refresh markers. Expired/revoked credential metadata remains available for owner review. Monitor these tables along with analytics storage. `npm run check` covers D1 and deployment routing; `npm run test:postgres` and `npm run test:hyperdrive` include the public API/MCP integration suite using isolated databases. The [original scope](https://github.com/dagurleo/yaap/blob/main/docs/API_MCP_SCOPE.md$4) records future endpoints for exports, saved views, annotations, site lifecycle and durable attribution; those features remain deferred. ## Local verification — 2026-09-11 Build, typecheck, D1 tests and deployment configuration passed: 55 tests passed, 3 PostgreSQL-specific tests skipped. PostgreSQL passed 35 tests; local Hyperdrive emulation passed 34. The dedicated API suite covers all 47 tool definitions and executes every read-only MCP tool through the official SDK with output-schema validation, plus representative mutations and authorization failures. Browser checks use an isolated D1 workspace and built client assets: create/revoke a personal token, register/remove an OAuth client, desktop/mobile layout, light/dark themes, and browser login → OAuth consent → registered callback. The callback-origin CSP regression is covered by the HTTP suite. Local PostgreSQL migration 0006 was applied with bounded lock/statement timeouts and verified. The unused local D1 database still needs migration 0018 if selected later. No deployment or production data changes. Website create/update accepts an optional IANA `timezone`. Site responses and report metadata return the saved timezone. Changing it changes calendar date boundaries for subsequent requests; report cursors from the previous timezone must be restarted. Capabilities advertises `timezone: "site"`.