HubSpot CMS for Developers study materials
HubSpot CMS for Developers study resources

Serverless Quote Form: Duplicate Deals and 429 Responses

HubSpot CMS for Developers · Put your knowledge to work in context.

All case studies

Serverless Quote Form: Duplicate Deals and 429 Responses

A manufacturer's CMS page hosts a custom quote calculator. The browser posts JSON to a HubSpot serverless function at /_hcms/api/quote. The function calls the HubSpot CRM API to create a deal, associate a contact, and create line items. The private app token is currently in a config.js file served to the browser. During a webinar, 1,500 submissions arrived in 12 minutes. The function returned many 429 responses and timed out; about 90 duplicate deals were created. The team also sees contacts created without deals. They want to keep the calculator fast and avoid duplicates, and security has asked why a write-capable token is in client-side code.

1. Address the token exposure and explain the correct serverless secret pattern.

Read the worked answer

A write-capable private app token in browser JavaScript is effectively public. Anyone can open DevTools, copy it, and create or modify CRM records. Removing it from the client is the first priority: rotate the token immediately, then store the new token as a serverless secret using the HubSpot CLI's secrets mechanism. The function reads it from process.env at runtime. The browser should only post quote data to /_hcms/api/quote; it should never receive the token or call HubSpot's CRM API directly.

Server-side validation also matters. The function should validate required fields, normalize email and phone, reject unexpected payload sizes, and avoid echoing CRM responses that contain sensitive data. The private app should have only the scopes the function needs: create and read contacts and deals, create line items, and associations. Do not reuse a broad admin token. Put the secret name in documentation but never in the repo, and confirm the token is not in config.js, build artifacts, or client-side bundles.

2. Diagnose the duplicates and 429 responses, and propose a retry and dedupe design that fits serverless limits.

Read the worked answer

1,500 submissions in 12 minutes is about two per second on average, but webinar traffic arrives in bursts. If each submission makes several CRM calls, the function can exceed the app's rate limit, receive 429s, and retry. If the client also retries after a timeout, the same quote can be submitted twice. The missing deals for some contacts suggest partial failure: the contact was created, then the deal call failed or timed out.

I would add an idempotency key generated by the browser per quote attempt, store it with the submission, and check it before creating anything. A HubDB table or custom object can act as a lightweight ledger, but the check and write must be fast. On 429, use bounded exponential backoff with jitter inside the function and respect Retry-After; do not retry indefinitely. Return a clear failure to the client rather than a generic success, and make the client reuse the same idempotency key when retrying. Reduce calls per submission where the API supports batching, and create the deal only after the contact is confirmed. Monitor 429 rates and function timeouts.

3. How would you make the calculator fast and resilient during the next webinar?

Read the worked answer

Keep the function's critical path short. Validate the payload, check idempotency, upsert the contact, create the deal, and return; move non-critical enrichment or line-item expansion to a follow-up process if the architecture allows. Cache static configuration such as product and pricing tables rather than querying them on every request. Set conservative timeouts on outbound API calls so a slow CRM response does not consume the whole function execution budget. If the function cannot finish, return a retryable error with a request ID instead of pretending success.

For the next webinar, test at the expected burst rate in a sandbox, add rate limiting per IP or session if abuse is plausible, and alert on 429s, timeouts, and duplicate-key hits. The client should disable double-submit and show a pending state. Most importantly, make the submission idempotent end to end: the same request ID must not create a second deal even if the browser retries or the function times out after the write. That is the difference between a slow function and a duplicate-ridden CRM.