FS-DOC-05Technical
Integration and API Spec
Customer Feedback Loop
[YourCompany.com] · Management Department · Prepared by FullSpec · [Today's Date]
This document is the definitive technical reference for every integration point in the Customer Feedback Loop automation. It covers authentication requirements, exact OAuth scopes, webhook configuration, field-level data mappings between tools, the credential store structure, and the error handling behaviour expected at each integration boundary. The FullSpec team uses this specification throughout the build, test, and monitoring phases. No credentials or IDs are hardcoded in workflow logic; all sensitive values live in the shared credential store described in Section 04.
01Tool inventory
Tool
Role in automation
Auth method
Min plan required
Used by agents
Orchestration layer
Hosts all workflow logic, credential store, and retry engine
Internal service account
N/A (platform choice)
All agents
Typeform
Trigger: captures structured survey responses
OAuth 2.0
Basic ($29/mo)
Agent 1 (Feedback Intake)
Gmail
Trigger: receives feedback-tagged emails; Action: sends acknowledgement emails
OAuth 2.0 (Google)
Google Workspace or free Gmail
Agent 1 (Intake), Agent 3 (Response)
Google Sheets
Central feedback log; inter-agent data handoff store
OAuth 2.0 (Google)
Free (Google account required)
Agent 1 (write), Agent 2 (read)
HubSpot
CRM contact lookup and record update with sentiment data
OAuth 2.0 (Private App token)
Starter CRM ($20/mo)
Agent 2 (Sentiment and Routing)
Slack
Delivers urgent Slack alert when sentiment score is 4 or below
Bot token (OAuth 2.0)
Free tier sufficient
Agent 2 (Sentiment and Routing)
Notion
Appends classified feedback entries to the live themes dashboard
OAuth 2.0 (Internal Integration)
Free tier sufficient
Agent 3 (Response and Reporting)
Before you connect anything: sandbox-test every connection using non-production credentials and dummy data before any live credentials are entered into the credential store. For tools that support sandbox or developer environments (Typeform, HubSpot), use those environments exclusively during Agent 1 and Agent 2 build phases. Only promote to production credentials after end-to-end QA is signed off.
02Per-tool integration detail
Typeform
Typeform is the primary survey intake channel. The automation listens for new responses via a registered webhook. The Typeform account must have at least one form configured to pass structured fields (customer name, email, feedback text, and optionally a rating field). The webhook payload is JSON and delivered to the orchestration layer endpoint per response submission.
Auth method
OAuth 2.0. Register the orchestration platform as an OAuth app in the Typeform Developer Portal. Store access_token and refresh_token in the credential store. Refresh token rotation is automatic; the platform must handle 401 responses by re-exchanging the refresh token before retrying.
Required scopes
responses:read forms:read webhooks:write webhooks:read
Webhook / trigger setup
POST a webhook registration to https://api.typeform.com/forms/{form_id}/webhooks with the orchestration layer inbound URL as 'url' and enabled: true. Set secret in the payload; validate each inbound request by comparing the Typeform-Signature header (HMAC-SHA256 of the raw body using the shared secret) before processing. Store the webhook secret in the credential store as TYPEFORM_WEBHOOK_SECRET.
Required configuration
TYPEFORM_FORM_ID stored in credential store (not hardcoded). The form must include fields mapped to: respondent_email, respondent_name, feedback_text, and optionally rating_score. Field reference IDs (e.g. field_abc123) must be recorded in the field mapping table and updated if the form is rebuilt.
Rate limits
Typeform API: 200 requests/minute per token. At ~120 responses/month (4/day average), throttling is not required. Webhook delivery is push-based and does not consume API quota.
Constraints
Webhook retries from Typeform side occur up to 3 times with exponential backoff if the endpoint returns a non-2xx. The orchestration layer must respond with HTTP 200 within 5 seconds or Typeform will mark delivery as failed. Heavy form redesigns that change field reference IDs will break the field mapping and require a mapping update.
// Inbound webhook payload (key fields)
event_id: string
form_id: string // must match TYPEFORM_FORM_ID
token: string
answers[].field.ref: string // mapped to respondent_email, respondent_name, feedback_text
answers[].text | .number | .email: string | number
submitted_at: ISO8601 timestamp
// Signature validation
Header: Typeform-Signature: sha256=<hmac>
Validate against TYPEFORM_WEBHOOK_SECRET before processing
Gmail
Gmail serves two roles: as a trigger source (Agent 1 polls for feedback-tagged inbound emails) and as the send channel for customer acknowledgement emails (Agent 3). A dedicated Gmail label 'feedback-intake' is applied by a Gmail filter to qualifying inbound messages. The orchestration platform polls for messages with this label on a 2-minute interval.
Auth method
OAuth 2.0 via Google Identity Platform. Register the orchestration platform as a Google Cloud OAuth 2.0 client (Web Application type). Store access_token and refresh_token in the credential store as GMAIL_ACCESS_TOKEN and GMAIL_REFRESH_TOKEN. Token lifespan is 1 hour; refresh automatically on 401.
Required scopes
https://www.googleapis.com/auth/gmail.readonly https://www.googleapis.com/auth/gmail.send https://www.googleapis.com/auth/gmail.labels https://www.googleapis.com/auth/gmail.modify
Webhook / trigger setup
Agent 1 uses Gmail API polling (GET /gmail/v1/users/me/messages?labelIds=feedback-intake&q=is:unread) on a 2-minute schedule. After processing, mark each message as read using PATCH /gmail/v1/users/me/messages/{id}/modify with removeLabelIds: ['UNREAD']. Do not use Gmail Push Notifications (Pub/Sub) unless volume exceeds 500 emails/month; polling is sufficient at current volume.
Required configuration
GMAIL_LABEL_ID for the 'feedback-intake' label stored in credential store (not the label name string, which can change). GMAIL_SENDER_ADDRESS for outbound sends. Three Gmail draft templates stored as GMAIL_TEMPLATE_POSITIVE_ID, GMAIL_TEMPLATE_NEUTRAL_ID, and GMAIL_TEMPLATE_NEGATIVE_ID (draft IDs or template body strings stored in credential store). Template placeholders: {{customer_name}}, {{feedback_theme}}, {{support_email}}.
Rate limits
Gmail API quota: 250 quota units/second per user; GET messages costs 5 units, SEND costs 100 units. At 120 responses/month and 2-minute polling, estimated daily quota usage is well below 1% of the per-day cap of 1,000,000,000 units. Throttling is not required at current volume.
Constraints
The Gmail filter creating the 'feedback-intake' label must be configured manually in the Gmail account settings before the agent is activated. The OAuth app must be verified by Google if the account is a Google Workspace org with restricted third-party app access; allow-list the OAuth client ID in the Workspace Admin Console. Sending via Gmail has a daily send cap of 500 (free) or 2,000 (Workspace) emails per day, far above current volume.
// Agent 1 inbound (poll result per message)
message.id: string
message.threadId: string
message.payload.headers[From]: string // extract respondent_email
message.payload.headers[Subject]: string
message.payload.body.data: base64url // decode to get feedback_text
message.internalDate: epoch ms // convert to ISO8601 for Sheets log
// Agent 3 outbound (send)
POST /gmail/v1/users/me/messages/send
body: { raw: base64url(MIME message) }
MIME To: respondent_email
MIME Subject: 'Thank you for your feedback'
MIME Body: template interpolated with customer_name, feedback_themeGoogle Sheets
Google Sheets is the central inter-agent data store. Agent 1 appends one row per feedback item. Agent 2 reads new rows (those with status column = 'pending') to begin sentiment processing. The sheet acts as the handoff boundary between Agent 1 and Agent 2. A single designated spreadsheet with a named tab 'feedback_log' is used.
Auth method
OAuth 2.0 via Google Identity Platform, same Google Cloud OAuth client as Gmail. Scopes are additive on the same token. Store as SHEETS_ACCESS_TOKEN and SHEETS_REFRESH_TOKEN (or share the same token if the same Google account is used for both Gmail and Sheets).
Required scopes
https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive.readonly
Webhook / trigger setup
No native webhook available. Agent 2 polls the sheet every 2 minutes using GET /v4/spreadsheets/{spreadsheetId}/values/feedback_log for rows where column J (status) = 'pending'. After processing, Agent 2 updates column J to 'processed' via PATCH /v4/spreadsheets/{spreadsheetId}/values/feedback_log!J{row}.
Required configuration
SHEETS_SPREADSHEET_ID stored in credential store. Tab name must be exactly 'feedback_log'. Column order is fixed (see field mapping table in Section 03). Do not add or remove columns without updating the field mapping. The spreadsheet must have the service account or OAuth user granted Editor access.
Rate limits
Sheets API: 300 read requests/minute per project, 60 write requests/minute per project. At 120 rows/month appended by Agent 1 and Agent 2 polling every 2 minutes, peak usage is approximately 30 read requests/hour. Throttling is not required at current volume.
Constraints
Row appends must use values: [[...]] array in row-major order matching the fixed column schema. Never use named range writes as they are brittle if rows are inserted above the range. Agent 2 must process rows in ascending order of row number to preserve FIFO sequencing.
// Agent 1 append (POST values:append)
range: 'feedback_log!A:J'
valueInputOption: 'RAW'
values: [[
timestamp, // col A: ISO8601
source, // col B: 'typeform' | 'gmail'
respondent_email, // col C
respondent_name, // col D
feedback_text, // col E
'', // col F: sentiment_score (blank, filled by Agent 2)
'', // col G: sentiment_label (blank, filled by Agent 2)
'', // col H: theme_tag (blank, filled by Agent 2)
'', // col I: hubspot_contact_id (blank, filled by Agent 2)
'pending' // col J: status
]]
// Agent 2 read
GET values/feedback_log -> filter rows where col J = 'pending'
// Agent 2 update after processing
PATCH values/feedback_log!F{row}:J{row}
values: [[sentiment_score, sentiment_label, theme_tag, hubspot_contact_id, 'processed']]Integration and API SpecPage 1 of 4
FS-DOC-05Technical
HubSpot
HubSpot is used by Agent 2 to look up the respondent by email address and, if a match is found, add a note to the contact record and update a custom contact property tracking the latest feedback sentiment score. A Private App token is used rather than a full OAuth app to reduce scope surface area.
Auth method
HubSpot Private App access token. Generate in HubSpot Settings > Integrations > Private Apps. Store as HUBSPOT_PRIVATE_APP_TOKEN in the credential store. Include as header 'Authorization: Bearer {HUBSPOT_PRIVATE_APP_TOKEN}' on all requests. Tokens do not expire but must be rotated if compromised.
Required scopes
crm.objects.contacts.read crm.objects.contacts.write crm.objects.notes.write crm.schemas.contacts.read
Webhook / trigger setup
No webhook required. Agent 2 initiates all calls outbound. Contact lookup: POST /crm/v3/objects/contacts/search with filter on email property. Note creation: POST /crm/v3/objects/notes then associate with contact via POST /crm/v3/associations/note/contact/batch/create.
Required configuration
HUBSPOT_FEEDBACK_SCORE_PROPERTY: the internal name of the custom contact property that stores the latest sentiment score (e.g. 'latest_feedback_score'). This property must be created in HubSpot before the agent runs (type: number). HUBSPOT_ALERT_OWNER_ID: the HubSpot owner ID to assign escalated notes to when sentiment score is 4 or below. Store both in credential store, not hardcoded.
Rate limits
HubSpot API: 100 requests/10 seconds and 150,000 requests/day on Starter plan. Agent 2 makes 2 to 3 API calls per feedback record (search, update, note). At 120 records/month (4/day), estimated usage is 12 requests/day. Throttling is not required at current volume. Implement a 200ms inter-request delay as a precaution.
Constraints
Contact matching is by email address only. If no contact is found (email not in HubSpot), Agent 2 must log the miss in Google Sheets (col I = 'no_match') and continue without creating a new contact record. Contact creation is out of scope for this automation. The custom property 'latest_feedback_score' must be of type 'number' and must exist before the first production run.
// Contact search
POST /crm/v3/objects/contacts/search
{ filterGroups: [{ filters: [{ propertyName: 'email', operator: 'EQ', value: respondent_email }] }] }
-> response.results[0].id // hubspot_contact_id, or null if no match
// Property update
PATCH /crm/v3/objects/contacts/{hubspot_contact_id}
{ properties: { latest_feedback_score: sentiment_score } }
// Note creation
POST /crm/v3/objects/notes
{ properties: { hs_note_body: 'Feedback score: {sentiment_score} | Theme: {theme_tag} | Source: {source}', hs_timestamp: timestamp } }
-> response.id // note_id
// Note-contact association
POST /crm/v3/associations/note/contact/batch/create
{ inputs: [{ from: { id: note_id }, to: { id: hubspot_contact_id }, type: 'NOTE_TO_CONTACT' }] }Slack
Slack is used by Agent 2 to post a structured alert to a designated channel when a feedback record has a sentiment score of 4 or below. The alert includes the customer name, sentiment score, theme tag, and a direct link to the source response for the reviewer. A dedicated Slack Bot is used.
Auth method
Slack Bot token (xoxb-...) generated via a Slack App with Bot Token Scopes. Create the app in the Slack API portal (api.slack.com/apps), install it to the workspace, and store the Bot User OAuth Token as SLACK_BOT_TOKEN in the credential store.
Required scopes
chat:write chat:write.public channels:read
Webhook / trigger setup
No inbound webhook. Agent 2 posts outbound to POST https://slack.com/api/chat.postMessage. The target channel ID (not name) must be stored as SLACK_ALERT_CHANNEL_ID in the credential store. Use Block Kit formatted messages for structured display of score, theme, and response link.
Required configuration
SLACK_BOT_TOKEN and SLACK_ALERT_CHANNEL_ID in credential store. The Slack app must be invited to the target channel (/invite @FeedbackBot) before the first run. The alert message template is defined in the workflow logic, not in Slack; no Slack-side template setup is required.
Rate limits
Slack API tier 3: 50 requests/minute for chat.postMessage. At 120 records/month with a subset triggering alerts (estimate: 20 to 30% negative, so 24 to 36 alerts/month), peak rate is negligible. Throttling is not required at current volume.
Constraints
The bot must be a member of the target channel or the post will return a channel_not_found or not_in_channel error. If the channel is private, the bot must be explicitly invited. Do not post to channels by name; always use the stored channel ID as channel names can change.
// Outbound message payload
POST https://slack.com/api/chat.postMessage
Authorization: Bearer {SLACK_BOT_TOKEN}
{
channel: SLACK_ALERT_CHANNEL_ID,
blocks: [
{ type: 'header', text: { type: 'plain_text', text: 'Urgent Feedback Alert' } },
{ type: 'section', fields: [
{ type: 'mrkdwn', text: '*Customer:* {respondent_name}' },
{ type: 'mrkdwn', text: '*Score:* {sentiment_score}/10' },
{ type: 'mrkdwn', text: '*Theme:* {theme_tag}' },
{ type: 'mrkdwn', text: '*Source:* {source}' }
]},
{ type: 'actions', elements: [
{ type: 'button', text: { text: 'View Response' }, url: response_url }
]}
]
}Notion
Notion is used by Agent 3 to append each classified feedback entry to a running themes dashboard database. The database is pre-created in Notion with the required properties. The Notion Internal Integration is granted access to the specific database page only, not the entire workspace.
Auth method
Notion Internal Integration token. Create in notion.so/my-integrations, set as Internal type, and share the target database page with the integration in Notion UI (Share > Invite). Store the token as NOTION_INTEGRATION_TOKEN and the database ID as NOTION_DATABASE_ID in the credential store.
Required scopes
read_content update_content insert_content (set at integration creation; Notion does not use OAuth scope strings but rather capability toggles: Content Capabilities: Read, Update, Insert enabled)
Webhook / trigger setup
No webhook. Agent 3 posts new database entries outbound via POST https://api.notion.com/v1/pages with the parent database ID. Notion-Version header must be set to '2022-06-28' on all requests.
Required configuration
NOTION_DATABASE_ID in credential store. The Notion database must be pre-built with these properties before the first run: Date (type: date), Source (type: select: 'typeform' | 'gmail'), Customer Name (type: title), Sentiment Score (type: number), Sentiment Label (type: select: 'positive' | 'neutral' | 'negative'), Theme Tag (type: select: pre-populated with agreed theme options), HubSpot Matched (type: checkbox). Property names must match exactly, including capitalisation.
Rate limits
Notion API: 3 requests/second average, burst to 90 requests/30 seconds. At 120 records/month appended by Agent 3, usage is approximately 4 requests/day. Throttling is not required at current volume.
Constraints
Notion select property options (Theme Tag, Sentiment Label) must be pre-populated in the database schema. If Agent 3 passes a theme_tag value not in the existing select options, Notion will reject the page creation with a validation_error. Add all anticipated theme values to the select property before go-live. New theme values discovered post-launch must be manually added to the Notion select options before they can be logged.
// Append new page to Notion database
POST https://api.notion.com/v1/pages
Notion-Version: 2022-06-28
Authorization: Bearer {NOTION_INTEGRATION_TOKEN}
{
parent: { database_id: NOTION_DATABASE_ID },
properties: {
'Customer Name': { title: [{ text: { content: respondent_name } }] },
'Date': { date: { start: timestamp } },
'Source': { select: { name: source } },
'Sentiment Score': { number: sentiment_score },
'Sentiment Label': { select: { name: sentiment_label } },
'Theme Tag': { select: { name: theme_tag } },
'HubSpot Matched': { checkbox: hubspot_matched_bool }
}
}03Field mappings between tools
The following tables define the exact field-level mappings at each agent handoff boundary. Use these as the authoritative reference for all transform logic in the orchestration layer. Field names in monospace represent the exact key names in API payloads or sheet column references.
Agent 1 handoff: Typeform and Gmail to Google Sheets
Source tool
Source field
Destination tool
Destination field
Typeform
submitted_at
Google Sheets
col A: timestamp
Typeform
(static: 'typeform')
Google Sheets
col B: source
Typeform
answers[ref=respondent_email].email
Google Sheets
col C: respondent_email
Typeform
answers[ref=respondent_name].text
Google Sheets
col D: respondent_name
Typeform
answers[ref=feedback_text].text
Google Sheets
col E: feedback_text
Gmail
message.internalDate (epoch->ISO8601)
Google Sheets
col A: timestamp
Gmail
(static: 'gmail')
Google Sheets
col B: source
Gmail
message.payload.headers[From] (extract email)
Google Sheets
col C: respondent_email
Gmail
message.payload.headers[From] (extract display name)
Google Sheets
col D: respondent_name
Gmail
message.payload.body.data (base64url decode)
Google Sheets
col E: feedback_text
(blank at intake)
(empty string)
Google Sheets
col F: sentiment_score
(blank at intake)
(empty string)
Google Sheets
col G: sentiment_label
(blank at intake)
(empty string)
Google Sheets
col H: theme_tag
(blank at intake)
(empty string)
Google Sheets
col I: hubspot_contact_id
(static: 'pending')
(constant)
Google Sheets
col J: status
Agent 2 handoff: Google Sheets to HubSpot
Source tool
Source field
Destination tool
Destination field
Google Sheets
col C: respondent_email
HubSpot
contacts search filter: email
Google Sheets
col F: sentiment_score (written by Agent 2)
HubSpot
properties.latest_feedback_score
Google Sheets
col H: theme_tag (written by Agent 2)
HubSpot
note body: theme segment
Google Sheets
col B: source
HubSpot
note body: source segment
Google Sheets
col A: timestamp
HubSpot
properties.hs_timestamp (on note)
HubSpot (response)
results[0].id
Google Sheets
col I: hubspot_contact_id
Agent 2 handoff: Google Sheets to Slack (negative path only)
Source tool
Source field
Destination tool
Destination field
Google Sheets
col D: respondent_name
Slack
blocks[section].fields: Customer
Google Sheets
col F: sentiment_score
Slack
blocks[section].fields: Score
Google Sheets
col H: theme_tag
Slack
blocks[section].fields: Theme
Google Sheets
col B: source
Slack
blocks[section].fields: Source
Google Sheets
(constructed URL from row data)
Slack
blocks[actions].button.url: response_url
Agent 3 handoff: Google Sheets to Gmail (acknowledgement send)
Source tool
Source field
Destination tool
Destination field
Google Sheets
col C: respondent_email
Gmail
MIME To header
Google Sheets
col D: respondent_name
Gmail
template placeholder: {{customer_name}}
Google Sheets
col G: sentiment_label
Gmail
template selector: GMAIL_TEMPLATE_POSITIVE_ID | NEUTRAL | NEGATIVE
Google Sheets
col H: theme_tag
Gmail
template placeholder: {{feedback_theme}}
Agent 3 handoff: Google Sheets to Notion
Source tool
Source field
Destination tool
Destination field
Google Sheets
col A: timestamp
Notion
properties.Date.date.start
Google Sheets
col B: source
Notion
properties.Source.select.name
Google Sheets
col D: respondent_name
Notion
properties.Customer Name.title[0].text.content
Google Sheets
col F: sentiment_score
Notion
properties.Sentiment Score.number
Google Sheets
col G: sentiment_label
Notion
properties.Sentiment Label.select.name
Google Sheets
col H: theme_tag
Notion
properties.Theme Tag.select.name
Google Sheets
col I: hubspot_contact_id (non-empty = true)
Notion
properties.HubSpot Matched.checkbox
Integration and API SpecPage 2 of 4
FS-DOC-05Technical
04Build stack and orchestration
Orchestration layout
Three discrete workflows, one per agent. Each workflow is independently deployable and restartable. Workflows share a single credential store; no credentials are duplicated across workflows. Workflow names: WF-01 Feedback Intake Agent, WF-02 Sentiment and Routing Agent, WF-03 Response and Reporting Agent.
WF-01 trigger mechanism
Dual-path: (1) Inbound webhook listener for Typeform POST events (push, instant). Signature validated using HMAC-SHA256 against TYPEFORM_WEBHOOK_SECRET before any processing. (2) Scheduled poll of Gmail API every 2 minutes for messages with label ID matching GMAIL_LABEL_ID and status UNREAD. Both paths write to the same Google Sheets append step.
WF-02 trigger mechanism
Scheduled poll of Google Sheets every 2 minutes. Reads all rows in 'feedback_log' tab where col J = 'pending'. Processes rows in ascending row-number order (FIFO). After processing each row, immediately updates col J to 'processing' before making external API calls to prevent duplicate processing if a concurrent poll fires.
WF-03 trigger mechanism
Scheduled poll of Google Sheets every 2 minutes. Reads all rows where col J = 'scored' (set by WF-02 on completion). Sends Gmail acknowledgement and appends Notion entry, then updates col J to 'complete'. This two-step status hand-off (pending > processing > scored > complete) ensures no row is processed twice across any workflow.
Credential store type
Platform-native encrypted secret store. All values referenced by key name only in workflow logic. No credential value appears in any workflow step configuration, log, or error message.
Inter-agent data boundary
Google Sheets 'feedback_log' tab is the sole data handoff point between all three agents. No agent calls another agent directly. Status column (col J) is the state machine field governing which workflow picks up a given row.
The 'processing' intermediate status on col J is critical for idempotency. If WF-02 crashes after setting col J to 'processing' but before completing, a dead-letter recovery step must reset col J to 'pending' after 10 minutes of no progress. Implement this as a separate scheduled cleanup workflow (WF-00 Stale Row Recovery) that runs every 15 minutes.
Credential store key manifest. Values are never logged, printed in error messages, or passed as URL parameters.
// Credential store contents (all values stored as encrypted secrets)
// Typeform
TYPEFORM_ACCESS_TOKEN // OAuth 2.0 access token
TYPEFORM_REFRESH_TOKEN // OAuth 2.0 refresh token
TYPEFORM_FORM_ID // Target form ID (e.g. 'abc12345')
TYPEFORM_WEBHOOK_SECRET // HMAC-SHA256 shared secret for signature validation
// Gmail
GMAIL_ACCESS_TOKEN // OAuth 2.0 access token (Google)
GMAIL_REFRESH_TOKEN // OAuth 2.0 refresh token (Google)
GMAIL_LABEL_ID // Label ID for 'feedback-intake' (e.g. 'Label_1234567890')
GMAIL_SENDER_ADDRESS // From address for outbound sends
GMAIL_TEMPLATE_POSITIVE_ID // Template body or draft ID for positive sentiment
GMAIL_TEMPLATE_NEUTRAL_ID // Template body or draft ID for neutral sentiment
GMAIL_TEMPLATE_NEGATIVE_ID // Template body or draft ID for negative sentiment
// Google Sheets
SHEETS_ACCESS_TOKEN // OAuth 2.0 access token (Google, may share with Gmail)
SHEETS_REFRESH_TOKEN // OAuth 2.0 refresh token (Google, may share with Gmail)
SHEETS_SPREADSHEET_ID // Target spreadsheet ID
// HubSpot
HUBSPOT_PRIVATE_APP_TOKEN // Private App access token
HUBSPOT_FEEDBACK_SCORE_PROPERTY // Internal name of custom contact property
HUBSPOT_ALERT_OWNER_ID // HubSpot owner ID for escalated note assignment
// Slack
SLACK_BOT_TOKEN // Bot User OAuth Token (xoxb-...)
SLACK_ALERT_CHANNEL_ID // Channel ID (not name) for urgent alerts
// Notion
NOTION_INTEGRATION_TOKEN // Internal Integration token
NOTION_DATABASE_ID // Target database ID for themes dashboard
// Sentiment scoring (AI layer)
AI_MODEL_ENDPOINT // Endpoint URL for sentiment scoring model
AI_MODEL_API_KEY // API key for the sentiment scoring service
SENTIMENT_NEGATIVE_THRESHOLD // Score threshold for Slack alert (default: 4)
05Error 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's execution log with the workflow name, step name, error code, and the row or event ID being processed at the time of failure. Where a retry strategy is defined, implement exponential backoff starting at the interval stated.
Integration
Scenario
Required behaviour
Typeform webhook
Webhook signature validation fails (HMAC mismatch)
Reject immediately with HTTP 400. Do not process the payload. Log the event ID and timestamp. Do not retry. Alert via Slack to SLACK_ALERT_CHANNEL_ID if more than 3 failures occur within 10 minutes (possible replay attack).
Typeform webhook
Orchestration layer returns non-2xx to Typeform (processing error)
Typeform will retry up to 3 times with exponential backoff (Typeform-managed). WF-01 must respond within 5 seconds with HTTP 200 after queuing the event internally, even if downstream Sheets append has not yet completed. Use an internal queue to decouple receipt from processing.
Gmail API (poll)
OAuth token expired (401 response)
Automatically exchange GMAIL_REFRESH_TOKEN for a new access token via POST to https://oauth2.googleapis.com/token. Retry the original request once with the new token. If refresh also fails (invalid_grant), pause WF-01 Gmail path and send a platform alert. Do not retry indefinitely.
Gmail API (send)
Send fails with 429 (rate limit) or 500 (server error)
Retry up to 3 times with exponential backoff: 30s, 60s, 120s. If all 3 retries fail, log the row ID and respondent_email, update col J to 'send_failed', and post a Slack alert to SLACK_ALERT_CHANNEL_ID with the customer name so a manual send can be made. Never discard the send silently.
Google Sheets (append)
Append returns 403 (permission error)
Do not retry. Log the full error response and halt WF-01 for that event. Post a platform alert immediately. This indicates the OAuth user has lost Editor access to the spreadsheet; a human must resolve the permission before processing resumes.
Google Sheets (poll)
Spreadsheet API returns 429 (quota exceeded)
Backoff for 60 seconds and retry the poll. If 3 consecutive polls fail with 429, pause WF-02 polling for 10 minutes then resume. Log all occurrences. At current volume this should not occur; if it does, review whether another process is consuming quota on the same project.
Google Sheets (stale 'processing' row)
Row stuck in 'processing' status for more than 10 minutes
WF-00 Stale Row Recovery (runs every 15 minutes) resets col J from 'processing' to 'pending' for any row where the timestamp in col A is more than 10 minutes old and status is still 'processing'. This allows WF-02 to re-pick the row on the next poll cycle. Log the row number and timestamp of each reset.
HubSpot API (contact search)
No contact found for respondent_email
Expected non-error path. Write 'no_match' to col I (hubspot_contact_id). Set Notion HubSpot Matched property to false. Continue workflow normally. Do not create a new HubSpot contact. Log the miss for periodic manual reconciliation.
HubSpot API (property update or note creation)
API returns 429 or 5xx
Retry up to 3 times with exponential backoff: 15s, 30s, 60s. If all retries fail, log the hubspot_contact_id and sentiment_score, write 'hubspot_error' to a separate error log sheet, and continue the rest of WF-02 (Slack alert and status update) so downstream agents are not blocked by a CRM failure.
Slack API (chat.postMessage)
Bot not in channel (not_in_channel error)
Do not retry. This is a configuration error. Log the error, update col J to 'slack_config_error', and send a platform-level alert to the operations email address (not Slack, since Slack is broken). Halt Slack alerts until the bot is re-invited to the channel and the workflow is manually restarted.
Slack API (chat.postMessage)
API returns 429 or 5xx
Retry up to 3 times with exponential backoff: 10s, 20s, 40s. If all retries fail, log the alert content (customer name, score, theme) to a 'failed_alerts' tab in Google Sheets so the reviewer can action it manually. Never silently drop an urgent alert.
Notion API (page creation)
Theme tag value not in select options (validation_error)
Do not retry. Log the theme_tag value that caused the failure. Update col J to 'notion_schema_error'. Post a Slack alert with the unrecognised theme value so it can be added to the Notion select property. Once added, the row can be reprocessed by resetting col J to 'scored'.
AI sentiment scoring
Scoring service returns error or timeout
Retry up to 2 times with 15s backoff. If both retries fail, assign a fallback sentiment_score of 5 (neutral) and sentiment_label of 'unscored'. Tag the row in col H as 'unscored' and continue the workflow so intake is not blocked. Log all unscored rows for manual review. Never halt the entire pipeline because the scoring layer is unavailable.
Unhandled exceptions must never fail silently. If any workflow step raises an exception not covered by the table above, the platform must catch the exception, log the full stack trace and the record being processed, set the affected row's col J status to 'error', and send a platform alert to support@gofullspec.com. The workflow must then move to the next record rather than halting entirely.
Integration and API SpecPage 3 of 4
FS-DOC-05Technical
Document reference
FS-DOC-05
Process
Customer Feedback Loop
Contact
support@gofullspec.com
Integration and API SpecPage 4 of 4