PitchPrfct
First-party SMS/CRM integration connected via API key — powers the dialer SMS tab and lands as the first entry in Project88's typed provider registry.
PitchPrfct is the first integration to land on Project88's typed
provider registry — a folder-per-provider layout under
src/integrations/ that drives a new Project 88 tab in the
Connections settings modal, alongside the existing Integrations
(Composio-managed) and Tools tabs.
The PitchPrfct entry covers all 46 endpoints across 8 resource groups
through a single allowlisted path-passthrough edge function
(pitchprfct-api), plus a HMAC-verified inbound webhook
(pitchprfct-webhook). It powers both the native
SMS app and the dialer's
SMS tab.
Connect
⌘K→ Settings → Connections → Project 88 tab.- Click Connect on the PitchPrfct card. The generic
ApiKeyConnectModalopens — it matches the project's condensedConfirmDialogstyling and runs a per-providervalidate()hook before persisting. - Paste your PitchPrfct API key and submit. The key transits the
pitchprfct-apiedge function exactly once (with avalidateKeyenvelope) to confirm it's live, then lands in Supabase Vault via the discriminator-shapedinsert_integration_api_keyRPC. The key never reaches the browser again after connect.
You'll see PitchPrfct in the Project 88 tab with a "Connected" status.
Where the credential lives
| Stored | Notes |
|---|---|
Supabase Vault, via insert_integration_api_key (migration 126) | Payload is the self-describing discriminator { type: 'api_key', key, provider } — distinct from the OAuth-shaped payload used by Gmail / Outlook / Google Calendar |
RPC permissions (migration 127) | REVOKE EXECUTE FROM anon; GRANT EXECUTE TO authenticated. Clears Supabase advisor lint 0028 |
| Edge function in-memory cache | Map<userId, key> with a 5-min TTL, scoped per warm instance |
The proxy never echoes the key back to the browser, and only allowlisted PitchPrfct paths can be reached through it.
Edge function: allowlisted path-passthrough
supabase/functions/pitchprfct-api is a single proxy with
verify_jwt: true. One regex covers all 46 endpoints across the 8
resource groups, and a 26-scenario Deno suite asserts the allowlist
plus a leak canary (the key must never appear in the response body or
in proxied error envelopes).
The browser-side typed client (src/integrations/pitchprfct/client.js)
is a hand-rolled JSDoc-typed JS wrapper (no TS in this codebase) with a
generic pollJob() helper for the bulk endpoints.
Drift guard
A nightly contract test under .github/workflows/contract-tests.yml
runs three live checks against PitchPrfct using the
PITCHPRFCT_TEST_API_KEY GitHub secret. If PitchPrfct ships a breaking
change, the workflow fails before the next user opens the dial widget.
Known upstream quirks
conversations.list({contactUuid})returns HTTP 500 for valid UUIDs. The dialer SMS tab works around it by skipping the conversations endpoint and callingmessages.list({contactUuid})directly — a comment at the call site flags the workaround so it can be unwound once upstream fixes the bug./conversationsintermittently hangs then recovers with a transient 5xx.pitchprfct-apiretries once on 5xx / timeout (never on 401 / 429) and logs the upstream 5xx path so the pattern is visible in edge logs.
Sending: fromNumber resolution
PitchPrfct's POST /api/v1/messages requires a fromNumber header
(per its OpenAPI spec).
Resolution runs in this order:
localStorage['dial.sms.pitchprfct.fromNumber']override — an explicit power-user pin for teams that need to force a specific number.- The number the conversation already lives on — a shared
deriveConversationNumberreads the latest outbound message'sfromNumber(falling back to an inbound message'stoNumber) from the already-fetched history. Zero extra round trips, and replies stay on the same thread on the lead's phone instead of hopping numbers between sends. - Phone-numbers API —
GET /api/v1/phone-numbersas a fallback for brand-new leads with no history to derive from.pickSendingNumberskipsenabled: falserows (released numbers stay listed withdisabledReason: "user_deleted") and prefers default-flagged, then warmed-up numbers. Results are cached account-wide via React Query with a 10-min stale window; an account with no numbers surfaces an actionable error and evicts the empty result from cache so adding a number and retrying re-fetches.
The typed client exposes a phoneNumbers resource (list /
get). The edge-function /api/v1/ allowlist already permits it —
no backend change or redeploy needed.
Aug 2026 API-key regression
PitchPrfct silently revoked GET /api/v1/phone-numbers for
existing API keys on Aug 9, 2026 (403 Forbidden; all other
endpoints unaffected — a nightly contract test caught the same 403
independently). Freshly generated keys work.
Because contacts / messages endpoints still respond, threads with any history send fine even before the API key is rotated — the conversation-number path in step 2 above never touches the phone-numbers API. Only first-texts to brand-new leads require the fresh key.
Error surfacing
The client extracts the real upstream error text (PitchPrfct is
NestJS — message may be a string or an array) instead of a bare
HTTP 400, and both useDialSmsForLead and the SMS app carry the
error message onto the failed bubble. Its retry-affordance tooltip
explains why — opted-out contact, insufficient credits, no
sending number — so a failed send never lands as an unexplained
"Failed" pill.
Client API corrections
Three off-spec calls in the typed client were fixed while wiring live messages:
conversations.updatenow exists (PATCH{isRead}) — used by the inbox rail's Unread tab and the "click marks read" flow.bulkUpdatemoved to POST per the upstream OpenAPI spec.- Star is contact-level (
PATCH /contacts/{uuid} {starred}) — it's a per-contact flag, not a per-conversation one. Every star toggle in the inbox rail resolves to the row's contact.
Adding another Project 88 integration
The registry layout is intentionally copy-paste-able. To wire a new first-party provider:
- Duplicate
src/integrations/pitchprfct/tosrc/integrations/<new-provider>/. - Swap the provider definition in
index.js(display name, icon,validate()hook, base URL). - Update the typed client wrapper and its tests.
- Add the new provider to
src/integrations/registry.js.
The same vault path, RPC, modal, and Project 88 tab pick it up automatically.
Webhooks
Inbound PitchPrfct deliveries push into Project88 in ~1 s via a Gmail-push-style pipeline:
PitchPrfct webhook → pitchprfct-webhook edge fn → sms_events
(migration 158) → Supabase Realtime → cache invalidation in
useSmsRealtime.
- HMAC-SHA256 verified on
{timestamp}.{body}with a replay window, plus a per-integration URL token. Events are idempotent on delivery id; 30-day retention. - Auto-registration. The first time an operator opens the SMS
surface, a JWT-authed register action resolves the user's vault
API key and idempotently creates or adopts the PitchPrfct
webhook (
message.*events). No manual dashboard step. Verified in prod: real signed deliveries land insms_eventsand drive the UI immediately. - Polling demoted to fallback — thread 20 s, inbox 60 s. The dial widget's SMS tab and the SMS app mount the same realtime hook.
pitchprfct-webhook runs with verify_jwt: false — the token + HMAC
combination is its authentication. sms_events and sms_webhook_state
were introduced by the sms_realtime migration and are on the
supabase_realtime publication.
Where to next
- SMS app — the native surface built on this integration
- Dial → SMS tab — the per-lead single-thread surface built on the same primitives
- Vault and secrets — how Project88 stores credentials in general
- Webhooks — the user-facing webhook trigger node for automations