FS-DOC-05Technical
Integration and API Spec
Customer Satisfaction Surveying
[YourCompany.com] · Customer Support Department · Prepared by FullSpec · [Today's Date]
This document is the authoritative technical reference for every integration point in the Customer Satisfaction Surveying automation. It covers tool authentication requirements, exact OAuth scopes, webhook configuration, field mappings between systems, orchestration layout, credential storage, and failure handling. The FullSpec team uses this spec to build and wire each integration; it should be treated as the single source of truth throughout the build and QA phases.
01Tool inventory
Tool
Role in automation
Auth method
Min plan required
Used by agent(s)
HubSpot
CRM trigger source; contact record read and write; survey-send log target; CSAT score write-back
OAuth 2.0 (private app token)
Starter (tickets pipeline required)
Agent 1, Agent 2
Typeform
Survey form host; real-time response capture via webhook
OAuth 2.0 / personal access token + webhook secret
Basic paid plan (webhooks required)
Agent 2
Gmail
Outbound personalised survey email delivery from business account
OAuth 2.0 (Google service account or user OAuth)
Google Workspace (any paid tier) or free Gmail with OAuth
Agent 1
Slack
Real-time detractor alert delivery to support manager channel
OAuth 2.0 (Bot token via Slack App)
Free tier sufficient; channel must exist
Agent 2
Google Sheets
Running satisfaction report; row append per survey response; formula-driven averages
OAuth 2.0 (service account JSON key)
Free (Google account required)
Agent 2
Automation platform (orchestration layer)
Hosts all workflow logic; stores credentials; executes triggers, branching, HTTP calls, and data transforms
Platform-native credential store
Paid plan with webhook listener support
Agent 1, Agent 2
Before you connect anything: every integration must be validated against a sandbox or staging environment before production credentials are entered. For HubSpot, use a developer sandbox account. For Typeform, use a test form in the same workspace. For Gmail, send to an internal test address. For Google Sheets, use a duplicate sheet with the same column schema. Only after all connections are verified in staging should production credentials be stored and activated.
02Per-tool integration detail
HubSpot
Used by both agents. Agent 1 (Survey Dispatch) polls for newly closed tickets and writes the survey-send log. Agent 2 (Response Processing) writes the CSAT score and verbatim comment back to the contact record.
Auth method
OAuth 2.0 via HubSpot Private App. Generate a private app token in HubSpot Settings > Integrations > Private Apps. Token is long-lived but should be rotated every 90 days. Store as HUBSPOT_PRIVATE_APP_TOKEN in the credential store. Never hardcode.
Required scopes
crm.objects.contacts.read, crm.objects.contacts.write, crm.objects.deals.read, crm.objects.deals.write, tickets, crm.schemas.contacts.read, crm.schemas.contacts.write
Trigger / webhook setup
Agent 1 polls the HubSpot Tickets API on a 2-minute interval: GET /crm/v3/objects/tickets?filter=hs_pipeline_stage:[CLOSED_STAGE_ID]&after=[last_cursor]. The exact pipeline stage ID must be retrieved from the HubSpot account during onboarding and stored as HUBSPOT_CLOSED_STAGE_ID. Do not hardcode the stage name string; use the numeric ID.
Required configuration
Custom contact properties required: csat_last_survey_sent (datetime), csat_score (number), csat_comment (single-line text), csat_ticket_ref (single-line text). Pipeline stage ID stored as HUBSPOT_CLOSED_STAGE_ID. All IDs stored in credential store.
Rate limits
HubSpot enforces 100 requests/10 seconds and 40,000 requests/day for Private Apps. At ~120 surveys/month (~4/day) and one poll every 2 minutes, expected daily call volume is well under 1,000. No throttling logic required at current volume, but implement a 1-second delay between batch write calls as a safeguard.
Constraints
Pipeline stage ID must be confirmed before build; stage names are not unique across HubSpot accounts. The eligibility check (surveyed in last 30 days) reads csat_last_survey_sent; if the property does not exist on older contacts it defaults to null and the contact is treated as eligible. GDPR data retention settings in the HubSpot account must be reviewed to ensure survey score properties are not auto-deleted.
// Poll input
GET /crm/v3/objects/tickets
?properties=hs_pipeline_stage,associations.contact,hs_ticket_id,hubspot_owner_id
&filterGroups[0][filters][0][propertyName]=hs_pipeline_stage
&filterGroups[0][filters][0][operator]=EQ
&filterGroups[0][filters][0][value]={{HUBSPOT_CLOSED_STAGE_ID}}
// Write survey-send log (Agent 1)
PATCH /crm/v3/objects/contacts/{contactId}
body: { properties: { csat_last_survey_sent: "{{iso_timestamp}}", csat_ticket_ref: "{{ticket_id}}" } }
// Write CSAT score (Agent 2)
PATCH /crm/v3/objects/contacts/{contactId}
body: { properties: { csat_score: "{{score}}", csat_comment: "{{comment}}" } }Typeform
Used by Agent 2 (Response Processing). Typeform delivers survey responses in real time via an outbound webhook to a listener endpoint on the automation platform.
Auth method
Personal access token for Typeform API management (creating and listing webhooks). Webhook payloads are verified using HMAC-SHA256 signature validation: the platform computes SHA256(secret, raw_body) and compares to the Typeform-Signature header. Store the webhook secret as TYPEFORM_WEBHOOK_SECRET in the credential store.
Required scopes
webhooks:read, webhooks:write, responses:read, forms:read
Webhook setup
Register the webhook via POST /forms/{formId}/webhooks/{tag} with the automation platform listener URL and TYPEFORM_WEBHOOK_SECRET. Set enabled: true. The form ID must be stored as TYPEFORM_FORM_ID. Tag the webhook as csat-response-handler. Verify the webhook from the Typeform dashboard after registration by sending a test payload and confirming the listener receives it.
Required configuration
TYPEFORM_FORM_ID and TYPEFORM_WEBHOOK_SECRET stored in credential store. Survey form must contain a hidden field named contact_id to pass the HubSpot contact ID through the survey link (appended as a query parameter when the email is sent). Field reference names must match those listed in Section 03.
Rate limits
Typeform API: 5 requests/second, 200 requests/minute. Webhook delivery is event-driven and not rate-limited. At ~120 responses/month, API call volume is negligible. No throttling needed.
Constraints
Webhooks require a paid Typeform plan (Basic or above). If the account is on a free plan the integration must fall back to polling the Responses API every 5 minutes, which increases detractor alert latency. This must be confirmed during onboarding. Hidden fields must be enabled in the form settings.
// Incoming webhook payload (abridged)
{
"event_id": "<uuid>",
"event_type": "form_response",
"form_response": {
"form_id": "{{TYPEFORM_FORM_ID}}",
"token": "<response_token>",
"submitted_at": "2024-01-22T10:34:00Z",
"hidden": { "contact_id": "<hubspot_contact_id>" },
"answers": [
{ "field": { "ref": "csat_score" }, "type": "number", "number": 4 },
{ "field": { "ref": "csat_comment" }, "type": "text", "text": "..." }
]
}
}
// Signature validation
expected = base64( HMAC-SHA256( TYPEFORM_WEBHOOK_SECRET, raw_body ) )
assert request.headers['Typeform-Signature'] == 'sha256=' + expectedGmail
Used by Agent 1 (Survey Dispatch). Sends the personalised survey email from the business Gmail account using the Gmail API. The Typeform survey link is embedded in the body with the HubSpot contact ID appended as a query parameter.
Auth method
OAuth 2.0. Use a dedicated Google service account with domain-wide delegation if sending on behalf of a shared address, or standard user OAuth if sending from a personal Workspace account. Store the OAuth refresh token (or service account JSON key) as GMAIL_OAUTH_REFRESH_TOKEN or GMAIL_SERVICE_ACCOUNT_KEY respectively.
Required scopes
https://www.googleapis.com/auth/gmail.send, https://www.googleapis.com/auth/gmail.compose
Webhook / trigger setup
Gmail is used as an action (send) only. No inbound webhook or trigger is required from Gmail. The send is initiated by the orchestration layer after eligibility is confirmed.
Required configuration
GMAIL_SENDER_ADDRESS stored in credential store (the From address). HTML email template stored in the platform as a reusable text asset with placeholders: {{customer_name}}, {{ticket_id}}, {{agent_name}}, {{typeform_survey_url}}?contact_id={{hubspot_contact_id}}. Template must not be hardcoded in the workflow node.
Rate limits
Gmail API: 250 quota units/second per user; sending via API costs 100 units/message. At ~120 emails/month (~4/day) the volume is well within daily sending limits (500 messages/day for free Gmail; 2,000/day for Workspace). No throttling needed.
Constraints
The sending account must have 'Less secure app access' disabled; OAuth is the only acceptable auth method. SPF and DKIM records for the sending domain should be verified before go-live to prevent survey emails landing in spam. If using a shared alias, domain-wide delegation must be explicitly granted in Google Admin.
// Gmail API send call
POST https://gmail.googleapis.com/gmail/v1/users/me/messages/send
Authorization: Bearer {{access_token}}
// RFC 2822 message (base64url encoded in 'raw' field)
From: {{GMAIL_SENDER_ADDRESS}}
To: {{customer_email}}
Subject: How did we do? Quick question for you, {{customer_name}}
Content-Type: text/html; charset=utf-8
<body>
Hi {{customer_name}}, ...
<a href="{{typeform_survey_url}}?contact_id={{hubspot_contact_id}}">Share your feedback</a>
</body>Integration and API SpecPage 1 of 3
FS-DOC-05Technical
Slack
Used by Agent 2 (Response Processing). Posts a formatted detractor alert message to a designated support manager channel when a CSAT score falls below the agreed threshold (score under 3).
Auth method
OAuth 2.0 Bot Token. Create a Slack App in the workspace, install it to the workspace, and store the Bot User OAuth Token as SLACK_BOT_TOKEN. The app requires no user-level permissions.
Required scopes
chat:write, chat:write.public (if posting to channels the bot has not been invited to)
Webhook / trigger setup
Slack is used as an action (post message) only. The bot posts to a specific channel ID stored as SLACK_DETRACTOR_CHANNEL_ID. Do not hardcode the channel name string; channel names can change. Retrieve the channel ID from the Slack API or the channel URL and store it in the credential store.
Required configuration
SLACK_BOT_TOKEN and SLACK_DETRACTOR_CHANNEL_ID stored in credential store. DETRACTOR_THRESHOLD stored as a configurable variable (default: 3; integer; scores strictly below this value trigger the alert). Message payload uses Block Kit for structured formatting.
Rate limits
Slack API: 1 message/second per channel (Tier 3 methods). At ~120 surveys/month with a typical 15-20% detractor rate, expected alert volume is ~18-24/month, well within limits. No throttling needed.
Constraints
The Slack app must be invited to the target channel before posting will succeed. If the channel is private the bot must be explicitly added. Alert must not be suppressed for any low-scoring response; if the Slack call fails it must be retried and logged (see Section 05).
// Slack Block Kit message payload
POST https://slack.com/api/chat.postMessage
Authorization: Bearer {{SLACK_BOT_TOKEN}}
{
"channel": "{{SLACK_DETRACTOR_CHANNEL_ID}}",
"blocks": [
{ "type": "header", "text": { "type": "plain_text", "text": ":warning: Detractor Alert" } },
{ "type": "section", "fields": [
{ "type": "mrkdwn", "text": "*Customer:*\n{{customer_name}}" },
{ "type": "mrkdwn", "text": "*CSAT Score:*\n{{score}}/5" },
{ "type": "mrkdwn", "text": "*Ticket Ref:*\n{{ticket_id}}" },
{ "type": "mrkdwn", "text": "*Comment:*\n{{comment}}" }
]}
]
}Google Sheets
Used by Agent 2 (Response Processing). Appends one row per survey response to a structured tracking sheet. Existing sheet formulas calculate running averages, response rates, and weekly summaries automatically.
Auth method
OAuth 2.0 via Google service account. Download the service account JSON key and store it as GOOGLE_SERVICE_ACCOUNT_KEY in the credential store. Share the target spreadsheet with the service account email address (Editor permission).
Required scopes
https://www.googleapis.com/auth/spreadsheets
Webhook / trigger setup
Google Sheets is used as an action (append row) only. No inbound trigger from Sheets is used in this automation.
Required configuration
GOOGLE_SHEETS_SPREADSHEET_ID and GOOGLE_SHEETS_RESPONSE_TAB_NAME stored in credential store. The spreadsheet must have the exact column schema defined in Section 03 before the first live run. Formula columns (average score, response rate) must occupy columns to the right of the data range and must not overlap the append range. Spreadsheet ID is found in the URL: docs.google.com/spreadsheets/d/{{SPREADSHEET_ID}}/edit.
Rate limits
Sheets API: 300 requests/minute per project, 60 requests/minute per user. At ~120 appends/month the volume is negligible. No throttling needed.
Constraints
The append call uses values:append with insertDataOption=INSERT_ROWS to prevent overwriting existing formula rows. The tab name must exactly match GOOGLE_SHEETS_RESPONSE_TAB_NAME. If the sheet is renamed the integration will fail silently unless error handling is in place (see Section 05).
// Sheets API row append
POST https://sheets.googleapis.com/v4/spreadsheets/{{GOOGLE_SHEETS_SPREADSHEET_ID}}/values/{{GOOGLE_SHEETS_RESPONSE_TAB_NAME}}!A:H:append
?valueInputOption=USER_ENTERED
?insertDataOption=INSERT_ROWS
Authorization: Bearer {{access_token}}
{
"values": [[
"{{submitted_at}}",
"{{hubspot_contact_id}}",
"{{customer_name}}",
"{{customer_email}}",
"{{ticket_id}}",
"{{csat_score}}",
"{{csat_comment}}",
"{{is_detractor}}"
]]
}03Field mappings between tools
The tables below define the exact field-level mappings for each agent handoff. All source and destination field names are given in their native format as used in API calls and workflow node configurations.
Agent 1 handoff: HubSpot ticket closed to Gmail send, then Gmail confirmation back to HubSpot
Source tool
Source field
Destination tool
Destination field
HubSpot (ticket)
`hs_ticket_id`
Gmail
Email body placeholder `{{ticket_id}}`
HubSpot (contact)
`firstname` + `lastname`
Gmail
Email body placeholder `{{customer_name}}`
HubSpot (contact)
`email`
Gmail
`To` header
HubSpot (ticket)
`hubspot_owner_id` -> resolved display name
Gmail
Email body placeholder `{{agent_name}}`
HubSpot (contact)
`hs_object_id`
Gmail / Typeform URL
Query param `contact_id` appended to survey link
Automation platform (timestamp)
Execution datetime (ISO 8601)
HubSpot (contact)
`csat_last_survey_sent`
HubSpot (ticket)
`hs_ticket_id`
HubSpot (contact)
`csat_ticket_ref`
Agent 2 handoff: Typeform response webhook to HubSpot, Google Sheets, and Slack
Source tool
Source field
Destination tool
Destination field
Typeform (hidden field)
`hidden.contact_id`
HubSpot (contact)
Record lookup key: `hs_object_id`
Typeform (answer)
`answers[ref=csat_score].number`
HubSpot (contact)
`csat_score`
Typeform (answer)
`answers[ref=csat_comment].text`
HubSpot (contact)
`csat_comment`
Typeform (answer)
`answers[ref=csat_score].number`
Google Sheets
Column F: `csat_score`
Typeform (answer)
`answers[ref=csat_comment].text`
Google Sheets
Column G: `csat_comment`
Typeform (response)
`form_response.submitted_at`
Google Sheets
Column A: `submitted_at`
Typeform (hidden field)
`hidden.contact_id`
Google Sheets
Column B: `hubspot_contact_id`
HubSpot (contact, resolved)
`firstname` + `lastname`
Google Sheets
Column C: `customer_name`
HubSpot (contact, resolved)
`email`
Google Sheets
Column D: `customer_email`
HubSpot (contact property)
`csat_ticket_ref`
Google Sheets
Column E: `ticket_id`
Automation platform (computed)
`csat_score` < `DETRACTOR_THRESHOLD` -> boolean
Google Sheets
Column H: `is_detractor` (TRUE/FALSE)
Typeform / HubSpot (resolved)
customer_name, csat_score, ticket_id, csat_comment
Slack
Block Kit fields in detractor alert message
The Google Sheets column order (A through H) must be set up exactly as specified before the first live append. Do not insert columns between A and H after the integration goes live; doing so will shift the append target and corrupt the data layout. Formula columns for running averages must begin at column I or beyond.
Integration and API SpecPage 2 of 3
FS-DOC-05Technical
04Build stack and orchestration
Orchestration layout
Two discrete workflows, one per agent. Workflow 1: Survey Dispatch Agent. Workflow 2: Response Processing Agent. Both workflows share a single credential store. Each workflow is independently deployable and restartable without affecting the other.
Agent 1 trigger mechanism
Poll-based. Workflow 1 polls the HubSpot Tickets API every 2 minutes using a scheduled trigger node. It stores a cursor (last processed ticket updated_at timestamp) in the platform's built-in state store to avoid re-processing closed tickets. On each run it fetches all tickets where hs_pipeline_stage equals HUBSPOT_CLOSED_STAGE_ID and updated_at is greater than the stored cursor.
Agent 2 trigger mechanism
Webhook-based. Workflow 2 exposes a dedicated HTTPS listener endpoint on the automation platform. The Typeform webhook is registered against this URL. On each incoming POST request the platform immediately validates the HMAC-SHA256 signature using TYPEFORM_WEBHOOK_SECRET before any processing begins. If signature validation fails the request is rejected with HTTP 401 and an error is logged. No retry is issued from the platform side; Typeform will retry failed webhook deliveries up to 3 times over 24 hours.
Webhook signature validation
Computed as: base64( HMAC-SHA256( TYPEFORM_WEBHOOK_SECRET, raw_request_body ) ). Compare to the value in the Typeform-Signature request header (format: 'sha256=<base64_value>'). Use a constant-time string comparison to prevent timing attacks.
Credential store
All secrets and environment-specific IDs are stored in the automation platform's native encrypted credential store. No credential is written inline in any workflow node. See code block below for the full contents.
Shared state
The HubSpot poll cursor (last_processed_cursor, ISO 8601 datetime string) is persisted in the platform's key-value state store under the key survey_dispatch_cursor. This value is updated at the end of each successful poll run.
Execution environment
Both workflows execute in the cloud-hosted automation platform environment. No local or on-premise runner is required. Each workflow node executes sequentially within its run; parallel branching is not used in this automation.
Logging and observability
Each workflow run is logged by the platform with input/output snapshots per node. Errors are surfaced in the platform's execution history. Failed runs must not be silently discarded; the platform must be configured to retain execution logs for a minimum of 30 days.
Credential store and state store reference. All secrets managed via the platform encrypted store.
// ====================================================
// CREDENTIAL STORE CONTENTS
// All values stored as encrypted secrets.
// Reference in workflow nodes using the key name only.
// Never paste raw values into workflow node config.
// ====================================================
HUBSPOT_PRIVATE_APP_TOKEN // HubSpot Private App token (rotate every 90 days)
HUBSPOT_CLOSED_STAGE_ID // Numeric pipeline stage ID for 'Closed' tickets
// Example: '123456789' (confirm from HubSpot account)
TYPEFORM_PERSONAL_ACCESS_TOKEN // Used for webhook registration and management API
TYPEFORM_FORM_ID // Form ID of the CSAT survey (from Typeform URL)
TYPEFORM_WEBHOOK_SECRET // HMAC-SHA256 secret registered with the webhook
GMAIL_OAUTH_REFRESH_TOKEN // OAuth 2.0 refresh token for Gmail sending account
// OR: GMAIL_SERVICE_ACCOUNT_KEY (JSON, base64-encoded)
GMAIL_SENDER_ADDRESS // From address e.g. support@[YourCompany.com]
SLACK_BOT_TOKEN // Bot User OAuth Token (xoxb-...)
SLACK_DETRACTOR_CHANNEL_ID // Channel ID (not name) e.g. 'C04XXXXXXXX'
GOOGLE_SERVICE_ACCOUNT_KEY // Service account JSON key (base64-encoded)
GOOGLE_SHEETS_SPREADSHEET_ID // From spreadsheet URL
GOOGLE_SHEETS_RESPONSE_TAB_NAME // Exact tab name e.g. 'CSAT Responses'
DETRACTOR_THRESHOLD // Integer; default 3; scores strictly below this alert
SURVEY_COOLDOWN_DAYS // Integer; default 30; minimum days between surveys
// per contact
// State store key (not a secret; stored in platform key-value store)
survey_dispatch_cursor // ISO 8601 datetime of last successfully processed ticket05Error handling and retry logic
Every integration point has a defined failure behaviour. Unhandled exceptions must never fail silently. All errors must be logged to the platform execution history with the full error response body and the input payload that caused the failure.
Integration
Scenario
Required behaviour
HubSpot: ticket poll
API returns HTTP 401 (invalid or expired token)
Halt workflow run. Log error with token key name. Do not retry automatically. Alert via platform email notification. Manual action: rotate HUBSPOT_PRIVATE_APP_TOKEN and re-enable workflow.
HubSpot: ticket poll
API returns HTTP 429 (rate limit exceeded)
Pause the current run. Retry after 30 seconds with exponential backoff (30s, 60s, 120s). Maximum 3 retries. If still failing after 3 retries, log and halt. No silent failure.
HubSpot: contact property read (eligibility check)
csat_last_survey_sent property missing on contact (null or not created yet)
Treat as eligible (no previous survey sent). Proceed with dispatch. Log the null result for audit purposes. Do not halt.
HubSpot: contact property write (survey log or score write-back)
PATCH request returns HTTP 400 (invalid property or value)
Log the full error response and input payload. Skip the write step. Continue remaining workflow steps. Flag the execution in the platform error log for manual review.
Gmail: survey email send
API returns HTTP 403 (insufficient OAuth scope or account suspended)
Halt email send. Log error with sender address. Do not retry. Create a platform alert (email or dashboard notification). Manual fallback: support team sends survey manually from Gmail. Log the missed send against the ticket ID.
Gmail: survey email send
API returns HTTP 500 or 503 (transient server error)
Retry up to 3 times with exponential backoff (15s, 30s, 60s). If all retries fail, log the ticket ID and customer email to a dedicated 'failed sends' tab in the Google Sheet for manual follow-up.
Typeform: incoming webhook
HMAC-SHA256 signature validation fails
Reject the request immediately with HTTP 401. Log the rejection including the source IP and raw signature header. Do not process the payload. No retry issued by the platform. Typeform will retry delivery up to 3 times; if repeated failures occur, investigate TYPEFORM_WEBHOOK_SECRET rotation.
Typeform: incoming webhook
hidden.contact_id field is missing or malformed in the payload
Log a warning with the full payload. Append the row to Google Sheets with contact_id left blank and flag the is_detractor column. Do not update HubSpot (no valid record key). Do not send a Slack alert for this response; log it for manual review.
Slack: detractor alert
API returns HTTP 404 (channel not found or bot not invited)
Log the error with the channel ID value. Do not silently discard. Retry once after 10 seconds. If retry fails, fall back to sending an alert email to a configured fallback address (SLACK_FALLBACK_EMAIL, stored in credential store). The CSAT data write to HubSpot and Sheets must still complete regardless of Slack failure.
Slack: detractor alert
API returns HTTP 429 (rate limit)
Pause and retry after the Retry-After header duration (typically 1-60 seconds). Maximum 3 retries. If all retries fail, log and send fallback email. Do not suppress the alert.
Google Sheets: row append
API returns HTTP 400 (range or tab name mismatch)
Log the full error and the row data that failed to append. Halt the append step. Write the failed row payload to the platform execution log. Manual fallback: operations team pastes the row manually. Do not retry automatically; the cause is likely a structural change to the sheet that requires investigation.
Any integration
Unhandled exception or unexpected HTTP status code not covered above
Catch all unhandled exceptions at the workflow level. Log the exception type, message, stack trace, and full input payload. Send a platform dashboard alert. Never fail silently. Manual review required within one business day of any unhandled exception appearing in the execution log.
Retry backoff implementation note: all retry delays must use true exponential backoff with jitter (add a random 0-5 second offset to each delay interval) to prevent multiple failed runs from hammering the same endpoint simultaneously after a shared outage window. The maximum total retry wait time for any single operation must not exceed 5 minutes.
Support contact: for questions about this specification or integration decisions during build, contact the FullSpec team at support@gofullspec.com.
Integration and API SpecPage 3 of 3