This document accompanies the Service's marketplace submission and explains the OAuth scope set the Service requests. It is read together with the Privacy Policy and the Terms of Service.
The Service is a read-only diagnostic audit utility for agencies that manage multiple client accounts inside a third-party CRM platform ("the platform"). The Service surfaces four rule-based diagnostic findings to the agency for internal review:
the configured threshold.
threshold.
without a follow-up message before the configured cut-off.
configured threshold.
The Service is read-only for CRM business data: it never creates, updates, or deletes contacts, conversations, opportunities, messages, notes, tasks, payments, or any other CRM business object. It requests exactly one write-suffix scope, oauth.write, solely as a narrow OAuth control-plane carve-out used to mint per-location tokens via POST /oauth/locationToken (see §2 and §4). It never sends messages on behalf of the agency or its client accounts, and never estimates revenue or financial impact from its findings.
The Service uses two distinct OAuth scope layers to apply the principle of least privilege at each stage of the OAuth lifecycle, so that the install authorize URL never carries a scope the platform has not registered for the app.
These are the scopes sent to the platform's authorize URL when an agency clicks "Connect your CRM".
| # | Scope | Use case | Persisted from this scope |
|---|---|---|---|
| 1 | locations.readonly | Enumerate the client accounts the agency selects for audit. | SHA-256 hash of the location id only. |
| 2 | oauth.readonly | Read GET /oauth/installedLocations to discover the per-location install records the agency selected at bulk install. | Hashed location id only. |
| 3 | oauth.write | OAuth control-plane only — POST /oauth/locationToken to mint a short-lived per-location token used by the read scopes below. The narrow carve-out is enforced in code and audit-logged on every call. The token is never used to write CRM business data. | Audit row only (no token bytes persisted). |
The oauth.write carve-out is narrow and audit-logged. It does not authorise creating, updating, or deleting any CRM business object (contacts, conversations, messages, opportunities, calendars, workflows). Any CRM-data write scope (*.write) is blocked by an application-level guard.
The agency-level access token returned from the authorize → callback exchange is used to mint per-location tokens against the platform's locationToken endpoint. No location-level data scope is needed at the install authorize step. The minted per-location token is cached encrypted (AES-256-GCM) with a short TTL and de-duplicated; token bytes are never logged. The token-mint flow is the only consumer of oauth.write and issues no CRM-data write.
These scopes are enforced at per-location-token issuance and endpoint-call levels. They are never sent to the authorize URL and are not selectable at agency install. The list is split into a strict set (required for the current detector slate) and a provisional set targeted for removal once live verification confirms the strict set is sufficient.
| # | Scope | Use case | Detector(s) | Data persisted from this scope |
|---|---|---|---|---|
| 2 | conversations.readonly | Detect unanswered inbound messages; verify presence (yes / no) of a post-appointment follow-up; detect inbound contacts without a booking. | Unanswered Inbound, No-show Follow-up Gap, Booking Gap | SHA-256 hash of the conversation id; last-message timestamp; direction enum (inbound / outbound). No message bodies, no contact identifiers, no sender / recipient PII is read into application memory. |
| 3 | opportunities.readonly | Detect open opportunities stale beyond configurable thresholds (default 14 days). | Stale Opportunity | SHA-256 hash of the opportunity id; status enum; pipeline / stage id (configuration references, not PII); update timestamps. The opportunity monetary-value field is not read; revenue is not aggregated. |
| 4 | calendars/events.readonly | Detect appointments ending in no-show / cancelled; verify booking presence for inbound contacts. | No-show Follow-up Gap, Booking Gap | SHA-256 hash of the event id; status enum; start / end timestamps; SHA-256 hash of the linked contact id. No event title, no notes field, no organizer / attendee PII is persisted. |
| 5 | calendars.readonly (provisional) | Resolve calendar-event filter requirements when location-only filtering proves insufficient. | No-show Follow-up Gap, Booking Gap | SHA-256 hash of the calendar id (configuration reference). To be dropped after live verification confirms location-only filtering is sufficient. |
| 6 | contacts.readonly (provisional) | Resolve event ↔ contact linkage when event payloads do not carry a contact id field directly. | Booking Gap | SHA-256 hash of the contact id only. To be dropped after live verification confirms the appointment payload already carries the contact id. |
The two layers are defined from a single source of truth in code, which enforces the per-app scope cap (≤ 8), a registry-membership check, and an audit-only allowlist. The install authorize URL receives Layer A scopes only; Layer B scopes never reach it.
The Service deliberately declines the following scopes because they are not needed for the diagnostic surface, and because requesting them would create policy or regulatory exposure the Service does not need:
| Scope | Reason it is not requested |
|---|---|
users.readonly | The message direction field on conversations identifies inbound vs outbound; staff identity is not needed for any detector. |
conversations/message.readonly | The documented grant is centred on recording / transcription endpoints, both telephony-adjacent. The Service has no audio / transcript surface. |
forms.readonly | Out of scope for the current detector set. |
workflows.readonly | Out of scope for the current detector set; would also create an indirect dependency on writeable surfaces. |
oauth.write (generic / CRM-data write use) | The generic write use is forbidden by app policy. oauth.write is requested only as the narrow OAuth control-plane carve-out for per-location token minting (see §2 Layer A); it is never used to write CRM business data. |
snapshots.write | Forbidden by app policy. |
Any CRM-data *.write / *.readwrite variant | Forbidden by app policy and blocked by an application-level guard. The Service is audit-only for CRM business data by design. |
The Service binds itself to four invariants at every persistence layer:
endpoints (conversations, opportunities, calendars, contacts) is GET. The only non-GET upstream call is POST /oauth/locationToken — the OAuth control-plane token mint authorised by the oauth.write carve-out (see §2 Layer A). There is no CRM-data write code path anywhere in the Service: it never creates, updates, or deletes contacts, conversations, opportunities, messages, notes, tasks, or payments, and an application-level guard blocks any CRM-data write scope from being added alongside the carve-out.
enums, and aggregate counts from the platform's API responses. Bodies, names, emails, and phone numbers are never read into application memory beyond the immediate response object, which is discarded once the metadata fields have been extracted.
body, name, email, phone, and similar) anywhere in the findings evidence-metadata store. Entity identifiers are stored as SHA-256 hashes; optional AES-256-GCM ciphertext columns are populated only when the operator has provisioned an encryption key.
and phone-shaped patterns as a defence-in-depth backstop. Token values, OAuth state, and authorization codes are never logged.
The OAuth tokens themselves are encrypted at rest with AES-256-GCM, bound to the installation so that a row or field swap fails decryption rather than silently surfacing the wrong token.
conversations.readonly)conversations.readonly is the only sensitive read-only scope in the Service's request set. It is permitted under an exact-match audit-only allowlist (not a wildcard). The Service satisfies the full audit-only contract:
metadata.
guard.
(The oauth.write control-plane carve-out mints tokens only and is never an outbound-messaging surface.)
Write and readwrite variants of the same namespace, and the recording / transcription variants, remain forbidden and are not requested.
The Service has no code path that sends an SMS, voice call, email, or in-app message on behalf of the agency or its client accounts. It does not mutate pipelines, contacts, appointments, calendars, or any other upstream resource. The Service's only outputs are:
The agency is the sole party that decides whether and how to act on findings inside the platform.
The Service does not:
The rule is enforced at three layers: detector code reads only status enums and timestamps; the AI system prompt is instructed not to estimate revenue or invent numbers; and an AI output post-check flags dollar-amount and revenue-extrapolation phrasing for agency review.
The Service uses a third-party large language model (Anthropic Claude) as an assistive draft generator only. The AI never decides whether a finding is legitimate; it summarizes the rule-based detector output for the agency's internal review. The contract:
neutral metadata only. A pre-check pass scans every nested string for email- and phone-shaped patterns and redacts them before the prompt is built.
advice, no automatic-message content, no upstream-vendor brand mention, no invented numbers, neutral terminology only.
without altering the markdown body.
deterministic markdown shape so development and CI runs do not require network access or upstream cost.
The Service's multi-client audit requires an agency-level (bulk) install — the path that audits every client account the agency selects. A single client-account direct install is accepted as a limited single-account fallback: it audits that one account only and is not the agency-wide (multi-client) path. The marketplace listing presents the agency-level install as the primary path.
The Service accepts AppInstall / AppUninstall lifecycle events forwarded by the platform (signature verified upstream). AppUninstall performs a soft uninstall; tokens are left encrypted at rest pending key rotation on written request. AppInstall is an idempotent log entry — the real install is the OAuth callback path.
The OAuth install flow is anchored on a server-minted state token with a short TTL, consumed on first lookup; missing, expired, or replayed states are rejected before any token exchange. The post-callback session cookie is HMAC-signed (SameSite=Lax, HttpOnly, Secure) and tokens at rest are AES-256-GCM encrypted. The Service is a confidential client and the platform binds CSRF protection to the state round-trip.
The policy contract above is enforced in code and tests, including:
cap, registry membership, and audit-only allowlist.
that recursively rejects forbidden keys in the findings evidence store.
GET for CRM data at the type level, so write methods are refused by the compiler; the single control-plane POST (/oauth/locationToken) is handled in the auth boundary.
and target identifiers, an action enum, and PII-pre-checked metadata — never raw identifiers or secret material.
limiting, request correlation ids, and fail-closed secret validation in production).
The Service was developed mock-first against a sandbox. The two provisional Layer B scopes (calendars.readonly, contacts.readonly) are scheduled to be dropped after live verification confirms they are not required, leaving an expected post-verification set of four scopes (Layer A: 1; Layer B strict: 3). Two defence layers protect against PII leakage when live responses arrive: a sanitize layer that projects raw responses onto a tightly-typed shape dropping every PII field, and a paranoid walker that throws if any PII field name is present even after the projection.
The Service is operated by Doptar.
For policy-level questions, see the Privacy Policy and the Terms of Service.