Skip to content

feat(hubspot): add Webhooks v3 Subscriptions API methods#89

Closed
d-klotz wants to merge 2 commits into
mainfrom
feat/hubspot-webhook-subscriptions
Closed

feat(hubspot): add Webhooks v3 Subscriptions API methods#89
d-klotz wants to merge 2 commits into
mainfrom
feat/hubspot-webhook-subscriptions

Conversation

@d-klotz

@d-klotz d-klotz commented May 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds three methods to the HubSpot Api class for managing app-level webhook subscriptions:

  • listWebhookSubscriptions({ appId, developerApiKey })
  • createWebhookSubscription({ appId, developerApiKey, subscriptionType, propertyName })
  • deleteWebhookSubscription({ appId, developerApiKey, subscriptionId })

Why

Webhooks v3 Subscriptions is the API HubSpot apps use to subscribe to lifecycle / property-change events on contacts/companies/deals/etc. Right now consumers of @friggframework/api-module-hubspot have to roll their own HTTP layer for these endpoints — defeating the api-module's purpose of being the single home for HubSpot HTTP surface.

Reference: https://developers.hubspot.com/docs/api-reference/webhooks-webhooks-v3

Design notes

Auth is different from the rest of this Api class. Most endpoints use the per-portal OAuth bearer (access_token set by the OAuth2Requester). Webhooks v3 Subscriptions is app-level, identified by the developer-account API key — same Authorization: Bearer header but a different identity. Rather than mutate instance state (access_token) per-call, the new methods take appId + developerApiKey as parameters and use fetch directly with explicit headers. The endpoint URLs stay declared in the URLs map for consistency.

Note on hapikey: HubSpot deprecated the hapikey query-param auth in Nov 2022. These methods use the modern Bearer-header form.

409 / 404 semantics:

  • createWebhookSubscription returns { status, data } instead of throwing on 409, so callers can treat "already exists" as success in race conditions (e.g. two parallel handlers racing to register the same subscription).
  • deleteWebhookSubscription silently swallows 404 (subscription already gone — desired state achieved).
  • Other non-2xx responses throw with ${status} ${response body} for context.

Test plan

  • 10 new unit tests in packages/v1-ready/hubspot/tests/webhook-subscriptions.test.js mocking global.fetch. Covers: happy-path GET/POST/DELETE, propertyName forwarding, 409 idempotency, 404 idempotency, error pathways.
  • Existing test suite untouched (the new methods don't interact with the OAuth2 flow).
  • End-to-end smoke against a real HubSpot dev account (manual, not in CI).

Downstream usage

This unblocks https://github.com/ClockworkRecruiting/integrations/pull/12 which adds webhook-driven ongoing sync between HubSpot and Clockwork via Frigg. Without these methods landing here, the downstream PR has to maintain its own HTTP surface for the same endpoints — exactly what api-modules exist to prevent.

Adds three methods to the HubSpot Api class:

  - listWebhookSubscriptions({ appId, developerApiKey })
  - createWebhookSubscription({ appId, developerApiKey, subscriptionType, propertyName })
  - deleteWebhookSubscription({ appId, developerApiKey, subscriptionId })

These hit /webhooks/v3/{appId}/subscriptions* and authenticate with the
developer-account API key as `Authorization: Bearer ${developerApiKey}`,
distinct from the per-portal OAuth bearer the rest of this Api class uses.
Endpoints accept appId + developerApiKey per-call so callers control
identity without mutating Api instance state.

Behavioural notes:
  - createWebhookSubscription returns { status, data } so callers can
    treat 409 (subscription already exists) as success without throwing.
  - deleteWebhookSubscription tolerates 404 (subscription already gone).
  - Other non-2xx responses throw with status + response body for context.

Refs: https://developers.hubspot.com/docs/api-reference/webhooks-webhooks-v3

10 unit tests covering happy-path, 404/409 idempotency, and error cases.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fc17cee675

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/v1-ready/hubspot/api.js Outdated
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ subscriptionDetails, enabled: true }),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Send the create-subscription payload with HubSpot field names

createWebhookSubscription posts { subscriptionDetails, enabled: true }, but the v3 subscriptions endpoint expects top-level fields like eventType, optional propertyName, and active. Because this method currently nests the event inside subscriptionDetails and uses enabled instead of active, valid calls can be rejected with 400 responses, so callers cannot reliably create webhook subscriptions through this API method.

Useful? React with 👍 / 👎.

@d-klotz

d-klotz commented May 21, 2026

Copy link
Copy Markdown
Contributor Author

Code review

Found 1 issue:

  1. createWebhookSubscription sends the wrong request body shape. HubSpot's v3 Subscriptions endpoint expects flat top-level fields { eventType, propertyName, active }. The PR posts { subscriptionDetails: { subscriptionType, propertyName }, enabled: true }subscriptionDetails is a v4 Journal concept, and enabled is not a valid field (the v3 flag is active). HubSpot will return 400. Verified against HubSpot's docs; also flagged by Codex (P1).

async createWebhookSubscription({ appId, developerApiKey, subscriptionType, propertyName }) {
const subscriptionDetails = propertyName
? { subscriptionType, propertyName }
: { subscriptionType };
const response = await this._developerFetch(
this.baseUrl + this.URLs.webhookSubscriptions(appId),
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ subscriptionDetails, enabled: true }),
},
developerApiKey
);
if (response.status === 409) {
return { status: 409, data: await this._safeJson(response) };
}
await this._requireOk(response, 'createWebhookSubscription');
return { status: response.status, data: await response.json() };

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

The v3 Subscriptions endpoint expects flat top-level fields, not the
v4-Journal-style nested `subscriptionDetails`. Codex P1 review on PR
flagged this, verified against HubSpot's docs.

Before (rejected with 400 by HubSpot):
  POST /webhooks/v3/{appId}/subscriptions
  { "subscriptionDetails": { "subscriptionType": "contact.creation" },
    "enabled": true }

After:
  POST /webhooks/v3/{appId}/subscriptions
  { "eventType": "contact.creation", "active": true }

Method parameter also renamed `subscriptionType` → `eventType` so the
api-module surface mirrors HubSpot's actual field names. The response
shape change (`eventType` at top level rather than nested under
`subscriptionDetails`) flows through to callers.

Tests updated.

Ref: #89 (comment)
@d-klotz

d-klotz commented May 21, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in 6d30d84. Body now { eventType, propertyName, active: true } per the v3 docs; method parameter renamed subscriptionTypeeventType so the api-module surface mirrors HubSpot's field names. 10/10 tests green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant