FS-DOC-05Technical
Integration and API Spec
Customer Testimonial Collection
[YourCompany.com] · Marketing Department · Prepared by FullSpec · [Today's Date]
This document is the authoritative integration reference for the Customer Testimonial Collection automation. It covers every tool connected to the workflow, the exact authentication method and OAuth scopes required for each, field-level mappings between agent handoffs, orchestration layout, credential store contents, and defined failure behaviours for every integration point. The FullSpec team uses this document during build and testing; it also serves as the ongoing maintenance reference once the automation is live.
01Tool inventory
Tool
Role in automation
Auth method
Min plan required
Used by agent(s)
HubSpot
CRM trigger source and contact record updates
OAuth 2.0 (private app token)
Starter (with deal pipelines)
Agent 1, Agent 2
Gmail
Outreach and follow-up email delivery
OAuth 2.0 (Google Workspace)
Google Workspace Business Starter
Agent 1
Typeform
Structured testimonial submission form
Webhook (HMAC-SHA256 signature) + API key
Basic (webhooks included)
Agent 1 (stop trigger), Agent 2
Slack
Approval card and team notifications
OAuth 2.0 (Slack app with bot token)
Free tier or above
Agent 2
Notion
Testimonial library storage
Internal integration token (Bearer)
Plus or above (for API access)
Agent 2
Automation platform
Workflow orchestration, step logic, scheduling, and credential store
Platform-managed (internal)
Per selected platform tier
All agents
Before you connect anything: configure and validate every integration against a sandbox or staging environment before introducing production credentials. HubSpot supports a sandbox account; Typeform and Slack support test workspaces and draft forms. Production credentials must not be entered into the orchestration layer until all sandbox tests pass.
02Per-tool integration detail
HubSpot
Used as the primary trigger source (deal stage change) and as the destination for CRM note creation and contact property updates. A private app token is preferred over legacy API keys. The token must be rotated every 365 days and stored in the credential store.
Auth method
OAuth 2.0 via HubSpot Private App token (Bearer header). Private app scope selection replaces the legacy hapikey pattern.
Required scopes
crm.objects.deals.read, crm.objects.deals.write, crm.objects.contacts.read, crm.objects.contacts.write, crm.objects.notes.write, timeline.read, timeline.write
Webhook / trigger setup
Subscribe to the crm.deal.propertyChange event filtered on dealstage. The exact internal value for 'Closed Won' must be retrieved from the HubSpot pipeline settings (Settings > CRM > Pipelines) and stored as HUBSPOT_DEAL_STAGE_ID in the credential store. Do not hardcode the label string.
Required configuration
Pipeline ID (HUBSPOT_PIPELINE_ID) and deal stage ID (HUBSPOT_DEAL_STAGE_ID) stored in credential store. Contact property 'testimonial_status' must be created in HubSpot as a single-line text field before build. Outreach note template ID stored as HUBSPOT_NOTE_TEMPLATE.
Rate limits
HubSpot enforces 110 requests/10 seconds and 40,000 requests/day on Starter. At ~30 deals/month (roughly 1 per day), peak usage is well under 1% of the daily cap. No throttling logic is required at current volume, but exponential backoff must be implemented for 429 responses.
Constraints
Private app tokens are workspace-scoped and cannot be shared across HubSpot portals. The workflow must use a dedicated service account, not a personal user token. Webhook payloads from HubSpot do not include full contact objects; a secondary GET to /crm/v3/contacts/{contactId} is required to retrieve personalisation fields.
// Inbound trigger payload (deal stage change webhook)
{
"objectId": "<deal_id>",
"propertyName": "dealstage",
"propertyValue": "<HUBSPOT_DEAL_STAGE_ID>",
"changeSource": "CRM_UI",
"eventId": "<event_id>",
"subscriptionType": "deal.propertyChange"
}
// Secondary GET for contact personalisation
GET /crm/v3/contacts/{contactId}?properties=firstname,lastname,email,company,hs_deal_name
// Note creation (POST)
POST /crm/v3/objects/notes
{
"properties": {
"hs_note_body": "Testimonial request sent via automation. Template: [template_name]. Typeform link: [typeform_url]",
"hs_timestamp": "<ISO8601_timestamp>"
},
"associations": [{ "to": { "id": "<contact_id>" }, "types": [{ "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 202 }] }]
}Gmail
Handles all outbound email delivery: the initial testimonial request, the day-7 follow-up, and the day-14 follow-up. Emails are sent from the authenticated marketing manager account using pre-approved templates stored in the credential store. The sequence halts automatically when a Typeform submission is received.
Auth method
OAuth 2.0 via Google Workspace. A dedicated service account with domain-wide delegation is recommended so the automation sends as the marketing manager's address without requiring the manager to re-authenticate.
Required scopes
https://www.googleapis.com/auth/gmail.send, https://www.googleapis.com/auth/gmail.readonly (readonly used only to confirm sequence-stop condition if webhook is unavailable as fallback)
Webhook / trigger setup
Gmail is an action target only, not an inbound trigger. Sequence timing is controlled by the orchestration layer using delayed execution nodes (day 0, day 7, day 14). Each delayed step checks for a Typeform submission flag on the HubSpot contact before sending. If the flag is set, the step is skipped.
Required configuration
GMAIL_FROM_ADDRESS stored in credential store. Three email templates stored as plain HTML in the orchestration layer or credential store: GMAIL_TEMPLATE_INITIAL, GMAIL_TEMPLATE_FOLLOWUP_D7, GMAIL_TEMPLATE_FOLLOWUP_D14. Typeform URL stored as TYPEFORM_FORM_URL with a contact-specific hidden field prefilled using the HubSpot contact ID.
Rate limits
Google Workspace allows 500 recipients/day per user via the Gmail API. At 30 sends/month (~1/day), this is negligible. No throttling needed. Gmail API quota is 250 quota units per second; a single send costs 100 units.
Constraints
Emails must include an unsubscribe or opt-out mechanism to comply with CAN-SPAM. The automation must not send if the HubSpot contact has a marketing opt-out flag set (hs_email_optout = true). This check must be the first condition evaluated in Agent 1 after the trigger fires.
// Gmail send payload (Users.messages.send)
POST https://gmail.googleapis.com/gmail/v1/users/me/messages/send
{
"raw": "<base64url_encoded_RFC2822_message>"
}
// Decoded message headers (example: initial request)
From: {{GMAIL_FROM_ADDRESS}}
To: {{contact.email}}
Subject: A quick favour, {{contact.firstname}}
Content-Type: text/html; charset=UTF-8
// Template variable substitutions
{{contact.firstname}} -> HubSpot contact firstname property
{{contact.company}} -> HubSpot contact company property
{{deal.name}} -> HubSpot deal hs_deal_name property
{{typeform.url}} -> TYPEFORM_FORM_URL + ?hubspot_id={{contact.id}}Integration and API SpecPage 1 of 4
FS-DOC-05Technical
Typeform
Provides the structured submission form that customers complete. The form captures the testimonial text, a permission checkbox, and the customer's name and role. A hidden field carries the HubSpot contact ID so the submission can be matched back to the correct contact record. Typeform fires a webhook to the orchestration layer on each submission.
Auth method
Webhook delivery authenticated via HMAC-SHA256 signature in the Typeform-Signature header. Inbound webhook validation must check this header on every request. The Typeform API (for form metadata retrieval) uses a personal access token (Bearer).
Required scopes
Typeform API personal access token requires: forms:read (retrieve form schema and response schema). Webhook delivery does not use OAuth; it uses the HMAC secret set at webhook registration time.
Webhook / trigger setup
Register a webhook in the Typeform dashboard under the form's Connect tab: endpoint URL = the orchestration layer's inbound webhook URL for Agent 2. Enable 'form_response' event type. Set the webhook secret and store it as TYPEFORM_WEBHOOK_SECRET in the credential store. The orchestration layer must respond with HTTP 200 within 30 seconds or Typeform will retry.
Required configuration
TYPEFORM_FORM_ID stored in credential store. TYPEFORM_WEBHOOK_SECRET stored in credential store. The form must be purpose-built for this process with exactly these fields: field ID tf_testimonial_text (Long text), field ID tf_permission (Yes/No), field ID tf_customer_name (Short text), field ID tf_customer_role (Short text), hidden field tf_hubspot_id (populated via URL query parameter hubspot_id).
Rate limits
Typeform Basic plan allows unlimited responses. The Typeform API allows 10 requests/second. At 30 submissions/month this is not a concern. No throttling required.
Constraints
The permission field (tf_permission) must be a mandatory yes/no field. If the value is not 'yes', the orchestration layer must route to the manual review path rather than the auto-approve path. Never assume permission is granted if the field is missing or null.
// Inbound webhook payload from Typeform (form_response event)
{
"event_id": "<uuid>",
"event_type": "form_response",
"form_response": {
"form_id": "{{TYPEFORM_FORM_ID}}",
"submitted_at": "<ISO8601_timestamp>",
"hidden": { "hubspot_id": "<contact_id>" },
"answers": [
{ "field": { "id": "tf_testimonial_text", "type": "long_text" }, "text": "<testimonial_text>" },
{ "field": { "id": "tf_permission", "type": "yes_no" }, "boolean": true },
{ "field": { "id": "tf_customer_name", "type": "short_text"}, "text": "<name>" },
{ "field": { "id": "tf_customer_role", "type": "short_text"}, "text": "<role>" }
]
}
}Slack
Receives formatted approval card messages from Agent 2 and delivers the marketing manager's Approve or Flag for Review decision back to the orchestration layer via Slack's interactivity endpoint. A custom Slack app must be created and installed into the workspace.
Auth method
OAuth 2.0. A Slack app is created in the Slack API dashboard and installed into the target workspace. The bot token (xoxb-) is stored as SLACK_BOT_TOKEN. The signing secret is stored as SLACK_SIGNING_SECRET for validating interactive payload requests.
Required scopes
Bot token scopes: chat:write, chat:write.public, channels:read, incoming-webhook. Interactive Components scope: no additional OAuth scope required, but Interactivity must be enabled in the Slack app settings with the Request URL set to the orchestration layer's interaction endpoint.
Webhook / trigger setup
Enable Interactivity in the Slack app settings. Set the Request URL to the orchestration layer's Slack interaction handler endpoint. The handler must validate the X-Slack-Signature header using SLACK_SIGNING_SECRET and HMAC-SHA256 on every incoming interaction payload.
Required configuration
SLACK_BOT_TOKEN stored in credential store. SLACK_SIGNING_SECRET stored in credential store. SLACK_APPROVAL_CHANNEL_ID stored in credential store (do not use the channel name string; use the channel ID). Block Kit message template for the approval card stored in the orchestration layer config.
Rate limits
Slack API tier 3 methods (chat.postMessage) allow 1 request/second. At 30 submissions/month the rate is negligible. No throttling needed. Interactive payload delivery from Slack is push-based and does not count against API quotas.
Constraints
The Slack app must have 'Message Actions' (interactive buttons) enabled. Workspaces with Enterprise Grid or strict app governance may require admin approval before the app can be installed. The interaction response must be returned within 3 seconds or Slack will display a timeout error to the user; if processing takes longer, respond immediately with HTTP 200 and a deferred response URL.
// Outbound approval card (chat.postMessage with Block Kit)
POST https://slack.com/api/chat.postMessage
{
"channel": "{{SLACK_APPROVAL_CHANNEL_ID}}",
"blocks": [
{ "type": "header", "text": { "type": "plain_text", "text": "New testimonial submission" } },
{ "type": "section", "fields": [
{ "type": "mrkdwn", "text": "*Customer:*\n{{tf_customer_name}}" },
{ "type": "mrkdwn", "text": "*Role:*\n{{tf_customer_role}}" }
]},
{ "type": "section", "text": { "type": "mrkdwn", "text": "*Testimonial:*\n{{tf_testimonial_text}}" }},
{ "type": "actions", "elements": [
{ "type": "button", "text": { "type": "plain_text", "text": "Approve" }, "style": "primary", "action_id": "testimonial_approve", "value": "{{submission_id}}" },
{ "type": "button", "text": { "type": "plain_text", "text": "Flag for Review" }, "style": "danger", "action_id": "testimonial_flag", "value": "{{submission_id}}" }
]}
]
}
// Inbound interaction payload (button click from manager)
{
"type": "block_actions",
"actions": [{ "action_id": "testimonial_approve", "value": "{{submission_id}}" }],
"user": { "id": "<slack_user_id>", "name": "<username>" }
}Notion
Serves as the testimonial library. Each approved submission is written as a new page in a dedicated Notion database. The database schema must be created and its ID stored in the credential store before build begins. The Notion integration token must be shared with the target database.
Auth method
Internal integration token (Bearer). A Notion integration is created at notion.so/my-integrations, the token is stored as NOTION_INTEGRATION_TOKEN, and the token must be explicitly shared with the testimonial library database page in Notion's Share settings.
Required scopes
Notion internal integrations use capability flags rather than OAuth scopes: Read content: enabled, Update content: enabled, Insert content: enabled. No user information capability is required.
Webhook / trigger setup
Notion does not natively emit webhooks. Notion is an action target only. The orchestration layer writes to Notion on the Approve branch after receiving confirmation from the Slack interaction handler.
Required configuration
NOTION_DATABASE_ID stored in credential store. The testimonial library database must contain these properties before build: 'Customer Name' (title), 'Role' (rich text), 'Testimonial' (rich text), 'Approved Date' (date), 'HubSpot Contact ID' (rich text), 'Permission Granted' (checkbox), 'Status' (select: Draft, Approved, Flagged). NOTION_INTEGRATION_TOKEN stored in credential store.
Rate limits
Notion API average rate limit is 3 requests/second. A single testimonial write uses 1 POST request. At 30 approvals/month the rate is negligible. No throttling required. Implement exponential backoff for 429 responses.
Constraints
The integration token must be re-shared with the database any time the database is moved or duplicated in Notion. Rich text fields have a maximum of 2,000 characters per block; if a testimonial text exceeds this, the content must be split across multiple rich_text array entries.
// Create page in testimonial database (POST)
POST https://api.notion.com/v1/pages
Authorization: Bearer {{NOTION_INTEGRATION_TOKEN}}
Notion-Version: 2022-06-28
{
"parent": { "database_id": "{{NOTION_DATABASE_ID}}" },
"properties": {
"Customer Name": { "title": [{ "text": { "content": "{{tf_customer_name}}" } }] },
"Role": { "rich_text": [{ "text": { "content": "{{tf_customer_role}}" } }] },
"Testimonial": { "rich_text": [{ "text": { "content": "{{tf_testimonial_text}}" } }] },
"Approved Date": { "date": { "start": "{{approved_at_iso8601}}" } },
"HubSpot Contact ID": { "rich_text": [{ "text": { "content": "{{hubspot_contact_id}}" } }] },
"Permission Granted": { "checkbox": true },
"Status": { "select": { "name": "Approved" } }
}
}Integration and API SpecPage 2 of 4
FS-DOC-05Technical
03Field mappings between tools
The tables below document field-level mappings for every agent handoff in the workflow. Field names are shown in monospace exactly as they appear in each tool's API or schema. Any transformation applied between source and destination is noted in the destination field column.
Agent 1 handoff: HubSpot deal stage change to Gmail outreach send (Outreach Sequence Agent)
Source tool
Source field
Destination tool
Destination field
HubSpot
`properties.dealstage`
Automation platform
`trigger.deal_stage_id` (compared to `HUBSPOT_DEAL_STAGE_ID`)
HubSpot
`properties.hs_object_id` (deal)
Automation platform
`ctx.deal_id`
HubSpot
`associations.contacts[0].id`
Automation platform
`ctx.contact_id`
HubSpot (secondary GET)
`properties.firstname`
Gmail template
`{{contact.firstname}}`
HubSpot (secondary GET)
`properties.lastname`
Gmail template
`{{contact.lastname}}`
HubSpot (secondary GET)
`properties.email`
Gmail
`To:` header
HubSpot (secondary GET)
`properties.company`
Gmail template
`{{contact.company}}`
HubSpot (secondary GET)
`properties.hs_deal_name`
Gmail template
`{{deal.name}}`
HubSpot (secondary GET)
`properties.hs_email_optout`
Automation platform
`ctx.email_optout` (if true, halt sequence)
Automation platform
`TYPEFORM_FORM_URL + ?hubspot_id=` + `ctx.contact_id`
Gmail template
`{{typeform.url}}`
Agent 1 handoff: Gmail send confirmation to HubSpot note creation (Outreach Sequence Agent)
Source tool
Source field
Destination tool
Destination field
Automation platform
`ctx.contact_id`
HubSpot notes API
`associations[].to.id` (contact association)
Automation platform
`ctx.send_timestamp`
HubSpot notes API
`properties.hs_timestamp`
Automation platform
`ctx.template_name`
HubSpot notes API
`properties.hs_note_body` (interpolated into note text)
Automation platform
`TYPEFORM_FORM_URL`
HubSpot notes API
`properties.hs_note_body` (interpolated into note text)
Agent 1 to Agent 2 handoff: Typeform submission webhook to Slack approval card (Submission Review and Library Agent)
Source tool
Source field
Destination tool
Destination field
Typeform webhook
`form_response.hidden.hubspot_id`
Automation platform
`ctx.contact_id`
Typeform webhook
`form_response.answers[tf_testimonial_text].text`
Slack Block Kit
`blocks[2].text.text` (testimonial body)
Typeform webhook
`form_response.answers[tf_customer_name].text`
Slack Block Kit
`blocks[1].fields[0].text` (customer name)
Typeform webhook
`form_response.answers[tf_customer_role].text`
Slack Block Kit
`blocks[1].fields[1].text` (role)
Typeform webhook
`form_response.answers[tf_permission].boolean`
Automation platform
`ctx.permission_granted` (routes to review if false)
Typeform webhook
`form_response.submitted_at`
Automation platform
`ctx.submitted_at`
Automation platform
`ctx.submission_id` (generated UUID)
Slack Block Kit
`blocks[3].elements[].value` (button action value)
Agent 2 handoff: Slack approval to Notion database write and HubSpot contact update (Submission Review and Library Agent)
Source tool
Source field
Destination tool
Destination field
Automation platform
`ctx.contact_id`
Notion API
`properties.HubSpot Contact ID` (rich_text)
Automation platform
`ctx.tf_customer_name`
Notion API
`properties.Customer Name` (title)
Automation platform
`ctx.tf_customer_role`
Notion API
`properties.Role` (rich_text)
Automation platform
`ctx.tf_testimonial_text`
Notion API
`properties.Testimonial` (rich_text)
Automation platform
`ctx.approved_at` (ISO8601, system clock)
Notion API
`properties.Approved Date` (date.start)
Automation platform
`ctx.permission_granted`
Notion API
`properties.Permission Granted` (checkbox)
Automation platform
Literal string `"Approved"`
Notion API
`properties.Status` (select.name)
Automation platform
`ctx.contact_id`
HubSpot contacts API
`properties.testimonial_status` = `"collected"`
Automation platform
`ctx.approved_at`
HubSpot contacts API
`properties.testimonial_approved_date`
Integration and API SpecPage 3 of 4
FS-DOC-05Technical
04Build stack and orchestration
Orchestration layout
Two discrete workflows, one per agent. Agent 1 (Outreach Sequence Agent) and Agent 2 (Submission Review and Library Agent) run as independent workflow instances. They communicate via shared context written to the credential store or a lightweight key-value state store keyed by HubSpot contact ID. This allows Agent 1 to check whether Agent 2 has received a Typeform submission before sending a follow-up reminder.
Shared credential store
A single credential store is shared across both agents. All API tokens, IDs, and secrets are referenced by environment variable name. No credential is hardcoded in workflow nodes. Access to the credential store is restricted to the automation platform service account.
Agent 1 trigger mechanism
Webhook (push). HubSpot fires a deal.propertyChange event to the orchestration layer's inbound webhook endpoint when a deal stage changes. The orchestration layer validates the X-HubSpot-Signature-v3 header using HMAC-SHA256 before processing. If signature validation fails, the request is rejected with HTTP 401 and logged.
Agent 2 trigger mechanism
Webhook (push). Typeform fires a form_response event to the orchestration layer's inbound webhook endpoint on submission. The orchestration layer validates the Typeform-Signature header using HMAC-SHA256 with TYPEFORM_WEBHOOK_SECRET before processing. Slack interactive payloads (button clicks) are received at a separate interaction endpoint and validated using SLACK_SIGNING_SECRET.
Follow-up timing mechanism
Agent 1 uses delayed execution nodes within the orchestration layer. After the initial email send, a 7-day wait node is inserted before the day-7 follow-up step, and a further 7-day wait node before the day-14 step. Before each delayed send, the workflow queries the shared state store for a key of the form `typeform_received:<contact_id>`. If the key exists, the send step is skipped and the workflow terminates cleanly.
State store schema
Key: `typeform_received:<hubspot_contact_id>`. Value: ISO8601 timestamp of Typeform submission. TTL: 90 days. This key is written by Agent 2 immediately on receiving a valid Typeform webhook. Agent 1 reads this key before each follow-up send.
Inbound webhook endpoints
Two dedicated endpoints are provisioned by the orchestration layer: one for HubSpot deal stage webhooks (Agent 1 trigger), one for Typeform submission webhooks (Agent 2 trigger). A third endpoint handles Slack interactive payloads. All three endpoints must be HTTPS only.
Credential store contents (all values stored as environment variables; none hardcoded in workflow nodes):
Credential store: all values are environment variables. Rotate tokens per each tool's recommended schedule.
# HubSpot
HUBSPOT_PRIVATE_APP_TOKEN=<hubspot_private_app_token>
HUBSPOT_PORTAL_ID=<hubspot_portal_id>
HUBSPOT_PIPELINE_ID=<pipeline_id>
HUBSPOT_DEAL_STAGE_ID=<closed_won_stage_internal_value>
HUBSPOT_WEBHOOK_SECRET=<hubspot_v3_signature_secret>
# Gmail / Google Workspace
GMAIL_CLIENT_ID=<google_oauth_client_id>
GMAIL_CLIENT_SECRET=<google_oauth_client_secret>
GMAIL_REFRESH_TOKEN=<google_oauth_refresh_token>
GMAIL_FROM_ADDRESS=marketing@[YourCompany.com]
GMAIL_TEMPLATE_INITIAL=<html_string_or_template_id>
GMAIL_TEMPLATE_FOLLOWUP_D7=<html_string_or_template_id>
GMAIL_TEMPLATE_FOLLOWUP_D14=<html_string_or_template_id>
# Typeform
TYPEFORM_PERSONAL_ACCESS_TOKEN=<typeform_pat>
TYPEFORM_FORM_ID=<form_id>
TYPEFORM_FORM_URL=https://[yourcompany].typeform.com/to/<form_id>
TYPEFORM_WEBHOOK_SECRET=<hmac_secret_set_at_webhook_registration>
# Slack
SLACK_BOT_TOKEN=xoxb-<slack_bot_token>
SLACK_SIGNING_SECRET=<slack_app_signing_secret>
SLACK_APPROVAL_CHANNEL_ID=<channel_id_not_channel_name>
# Notion
NOTION_INTEGRATION_TOKEN=secret_<notion_internal_token>
NOTION_DATABASE_ID=<testimonial_library_database_id>
# State store
STATE_STORE_TTL_DAYS=90
05Error handling and retry logic
Unhandled exceptions must never fail silently. Every integration point must emit a structured error log entry and, where defined below, send a fallback Slack alert to SLACK_APPROVAL_CHANNEL_ID. The orchestration layer must capture the full error payload, the workflow step name, the contact ID, and the UTC timestamp for every failure event.
Integration
Scenario
Required behaviour
HubSpot (webhook inbound)
Invalid or missing X-HubSpot-Signature-v3 header
Reject with HTTP 401. Log the raw headers and source IP. Do not process the payload. Alert is not required for individual failures but repeated failures within 5 minutes must trigger a Slack alert to the ops channel.
HubSpot (deal stage check)
Deal stage value does not match HUBSPOT_DEAL_STAGE_ID
Discard the event silently. No retry, no alert. Log the received stage value for audit purposes.
HubSpot (contact GET)
404 contact not found or 403 permission denied
Halt Agent 1 for this contact. Log the deal ID, contact ID, and HTTP status. Send a Slack alert: 'Agent 1: Contact lookup failed for deal [deal_id]. Manual check required.' Do not send any email.
HubSpot (note creation)
429 rate limit or 5xx server error
Retry with exponential backoff: wait 5 seconds, then 15 seconds, then 60 seconds. After three failed attempts, log the failure and send a Slack alert. The outreach email has already sent at this point; the note failure must not block or roll back the email.
Gmail (email send)
401 token expired or revoked
Attempt token refresh using the stored refresh token. If refresh fails, halt the send. Log the error with contact ID and send a Slack alert: 'Agent 1: Gmail authentication failed. Refresh token may be revoked. Manual intervention required.' No retry until credentials are confirmed.
Gmail (email send)
429 quota exceeded or 5xx error
Retry with exponential backoff: 10 seconds, 30 seconds, 120 seconds. After three failed attempts, mark the outreach as pending and alert via Slack. Retry once more after 24 hours before escalating to manual fallback.
Typeform (webhook inbound)
Invalid or missing Typeform-Signature header
Reject with HTTP 400. Log the full headers and payload hash. Do not process the submission. Three consecutive invalid signature events within 10 minutes must trigger a Slack alert: 'Agent 2: Suspicious Typeform webhook activity detected.'
Typeform (webhook inbound)
Orchestration layer fails to respond within 30 seconds
Typeform will retry up to three times with a 5-minute interval. The orchestration layer must be idempotent: check for the submission ID in the state store before processing to prevent duplicate Slack cards and duplicate Notion entries.
Slack (approval card send)
Channel not found or bot not in channel
Log the channel ID and HTTP response. Send a fallback email to GMAIL_FROM_ADDRESS with the full submission details and a note that Slack delivery failed. This ensures the marketing manager can still review and approve manually.
Slack (interaction payload)
No button click received within 72 hours
Resend the Slack approval card as a reminder with a note that the submission is 72 hours old. If no action is taken within a further 48 hours (120 hours total), mark the submission as 'Pending Review' in Notion and send a final Slack alert to the ops channel.
Notion (page creation)
400 property validation error (e.g. field name mismatch)
Log the full API response including the offending field name. Do not retry automatically. Send a Slack alert: 'Agent 2: Notion write failed for [customer_name]. Schema mismatch detected. Manual entry required.' The HubSpot contact update must still proceed.
Notion (page creation)
429 rate limit or 5xx server error
Retry with exponential backoff: 5 seconds, 20 seconds, 90 seconds. After three failed attempts, log the full submission payload to the orchestration layer's persistent log and send a Slack alert with the testimonial text included so the marketing manager can enter it manually if needed.
HubSpot (contact property update)
Contact property testimonial_status does not exist
Log a descriptive error: 'HubSpot contact property testimonial_status not found. Property must be created in HubSpot before go-live.' Send a Slack alert. Do not retry; the property must be created manually and the step re-run for affected contacts.
For questions about this specification or to report a discrepancy, contact the FullSpec team at support@gofullspec.com. Reference the document title and the relevant section number in your message.
Integration and API SpecPage 4 of 4