-
Notifications
You must be signed in to change notification settings - Fork 0
Webform inbound API
For callout API refer to: https://github.com/Hway-Digital/stood-docs/wiki/Webhook-Callout-API
Version: 1.0
Last Updated: November 2025
Base URL: https://[REGION]-[PROJECT-ID].cloudfunctions.net
- Overview
- Authentication
- Base URL Structure
- Endpoints
- Request Format
- Response Format
- Rate Limiting
- Error Handling
- Business Logic
- Code Examples
- Postman Collection
- Best Practices
- Troubleshooting
The Stood CRM Web Form API allows you to programmatically submit contact, account, and deal information to your Stood CRM. This is ideal for:
- Website contact forms
- Lead capture pages
- Third-party integrations
- Marketing automation platforms
- E-commerce order processing
Key Features:
- ✅ Automatic deduplication of contacts and accounts
- ✅ Support for custom fields
- ✅ Flexible field mapping with dot notation
- ✅ Built-in rate limiting and DDoS protection
- ✅ Comprehensive validation
- ✅ Automatic deal creation with source tracking
All API requests require two authentication parameters:
| Parameter | Type | Location | Description |
|---|---|---|---|
teamId |
string | Body | Your Stood CRM Team ID |
teamKey |
string | Body | Your Web Form API Key (webFormKey) |
- Log in to Stood CRM
- Go to Admin → Team Management
- Your Team ID is displayed in the team card header (e.g.,
zDdmpWNC5RcXSpTeklPw) - Click the copy icon to copy it
- Log in to Stood CRM
- Go to Admin → Team Management
- Click on your team card
- Click on the Marketing Source section (orange box with campaign icon)
- Click "Generate" under WebForm API Key section
- Copy the generated key
- Click Save
⚠️ Security Warning: Keep your API Key secret! Anyone with this key can submit data to your CRM. Never expose it in client-side code or public repositories.
Your base URL follows this format:
https://[REGION]-[PROJECT-ID].cloudfunctions.net
- Go to Firebase Console
- Select your Stood CRM project
- Go to Build → Functions
- Click on the
webFormSubmitfunction - Extract the region and project ID from the function URL
Example:
Function URL: https://webformsubmit-ccffslccvq-od.a.run.app
Project ID: stood-abcd
Region: europe-west9
Base URL: https://europe-west9-stood-abcd.cloudfunctions.net
| Region | Location |
|---|---|
us-central1 |
Iowa, USA |
us-east1 |
South Carolina, USA |
europe-west1 |
Belgium |
europe-west2 |
London, UK |
europe-west9 |
Paris, France |
asia-east1 |
Taiwan |
asia-northeast1 |
Tokyo, Japan |
Creates or updates contacts, accounts, and deals in Stood CRM.
Endpoint: POST /webFormSubmit
Full URL: https://[REGION]-[PROJECT-ID].cloudfunctions.net/webFormSubmit
Content-Type: application/json
Rate Limit:
- 100 requests per minute per team
At minimum, you must provide either:
-
contact.emailORcontact.phone - AND at least one of:
contact.firstName,contact.lastName, ORaccount.name
{
"teamId": "your-team-id",
"teamKey": "your-api-key",
"formData": {
"contact.email": "john@example.com",
"contact.firstName": "John",
"contact.lastName": "Doe"
}
}{
"teamId": "zDdmpWNC5RcXSpTeklPw",
"teamKey": "sk_live_abc123def456xyz789",
"formData": {
"contact.firstName": "John",
"contact.lastName": "Doe",
"contact.email": "john.doe@acme.com",
"contact.phone": "+1-555-123-4567",
"contact.role": "CTO",
"contact.location": "New York, NY",
"account.name": "Acme Corporation",
"account.location": "New York, NY",
"account.website": "https://acme.com",
"account.description": "Leading provider of roadrunner traps",
"deal.name": "Acme Corp - Enterprise Plan",
"deal.amount": 50000,
"deal.stage": "s1",
"deal.description": "Interested in our enterprise solution for Q1 2025",
"deal.solution": "Enterprise Plan + Premium Support",
"deal.closingDate": "2025-03-31",
"deal.owner": "user-id-of-sales-rep",
"deal.tags": ["enterprise", "high-priority"],
"sourceName": "Website Contact Form"
}
}{
"teamId": "zDdmpWNC5RcXSpTeklPw",
"teamKey": "sk_live_abc123def456xyz789",
"formData": {
"contact.email": "jane@startup.io",
"contact.firstName": "Jane",
"contact.lastName": "Smith",
"account.name": "Startup Inc",
"deal.name": "Startup Inc - Annual License",
"deal.partnerKey": "PARTNER-2024-001",
"deal.industry": "SaaS",
"deal.referralSource": "LinkedIn Campaign",
"account.companySize": "50-100",
"account.annualRevenue": "5M-10M",
"sourceName": "LinkedIn Ads Q4"
}
}Fields that map directly to entity properties in Firestore.
| Field | Type | Required | Description |
|---|---|---|---|
contact.firstName |
string | Conditional* | Contact's first name (max 500 chars) |
contact.lastName |
string | Conditional* | Contact's last name (max 500 chars) |
contact.email |
string | Conditional** | Valid email address |
contact.phone |
string | Conditional** | Phone number (any format) |
contact.role |
string | No | Job title or role |
contact.location |
string | No | City, state, or full address |
* At least one name field OR account.name is required
** Either email OR phone is required
| Field | Type | Required | Description |
|---|---|---|---|
account.name |
string | Conditional* | Account/company name (max 500 chars) |
account.location |
string | No | Company location |
account.website |
string | No | Company website URL |
account.description |
string | No | Account description or notes (max 500 chars) |
account.parent |
string | No | Parent account ID (for subsidiaries) |
* Required if no contact.lastName provided
| Field | Type | Required | Description |
|---|---|---|---|
deal.name |
string | No | Deal name (auto-generated if not provided) |
deal.amount |
number | No | Deal value in team's currency (default: 0) |
deal.stage |
string | No | Deal stage: s0, s1, s2, s3, s4 (default: s0) |
deal.description |
string | No | Deal description or notes (max 500 chars) |
deal.solution |
string | No | Proposed solution (max 500 chars) |
deal.closingDate |
string | No | Expected close date (ISO 8601 or YYYY-MM-DD format) |
deal.tags |
array | No | Array of tag strings |
deal.owner |
string | No | User ID of the deal owner |
deal.parent |
string | No | Parent deal ID (for sub-deals) |
| Stage | Label | Description |
|---|---|---|
s0 |
Lead/Prospect | Initial contact (default) |
s1 |
Qualified | Qualified opportunity |
s2 |
Proposal | Proposal sent |
s3 |
Won | Deal closed successfully |
s4 |
Lost | Deal closed unsuccessfully |
| Field | Type | Required | Description |
|---|---|---|---|
sourceName |
string | No | Marketing source/campaign name (used in deal naming and tracking) |
Any field not listed as a standard field is treated as a custom field and stored in the entity's customFields object.
Use dot notation to specify the entity and field name:
entity.fieldName
Examples:
-
deal.partnerKey→ Stored in deal's customFields as{ partnerKey: "value" } -
account.industry→ Stored in account's customFields as{ industry: "value" } -
contact.linkedin→ Stored in contact's customFields as{ linkedin: "value" }
{
"teamId": "your-team-id",
"teamKey": "your-api-key",
"formData": {
"contact.email": "user@company.com",
"contact.firstName": "Alice",
"deal.partnerKey": "PARTNER123",
"deal.leadScore": "85",
"deal.campaignId": "SUMMER2024",
"account.industry": "Technology",
"account.employeeCount": "500-1000",
"account.annualRevenue": "50M-100M"
}
}Result in Firestore:
// Deal document
{
name: "Company - ...",
stage: "s0",
customFields: {
partnerKey: "PARTNER123",
leadScore: "85",
campaignId: "SUMMER2024"
}
}
// Account document
{
name: "Company",
customFields: {
industry: "Technology",
employeeCount: "500-1000",
annualRevenue: "50M-100M"
}
}HTTP Status: 200 OK
{
"success": true,
"accountId": "abc123def456",
"contactId": "xyz789ghi012",
"dealId": "jkl345mno678",
"isNewContact": true,
"isNewAccount": true
}| Field | Type | Description |
|---|---|---|
success |
boolean | Always true for successful requests |
accountId |
string | ID of created/updated account (may be empty) |
contactId |
string | ID of created/updated contact |
dealId |
string | ID of newly created deal |
isNewContact |
boolean |
true if contact was created, false if updated |
isNewAccount |
boolean |
true if account was created |
HTTP Status: 4xx or 5xx
{
"success": false,
"error": "Error message describing what went wrong"
}| HTTP Status | Error Message | Cause |
|---|---|---|
403 |
Invalid team credentials | Incorrect teamId or teamKey
|
400 |
Either contact.email or contact.phone is required | Missing required contact identifier |
400 |
At least contact.firstName, contact.lastName, or account.name is required | Missing required name field |
400 |
Invalid email format | Email doesn't match validation regex |
429 |
Too many submissions from this IP | Rate limit exceeded (IP) |
429 |
Too many submissions for this team | Rate limit exceeded (team) |
500 |
Internal server error | Server-side error (check logs) |
To prevent abuse and ensure fair usage, the API implements rate limiting:
| Limit Type | Threshold | Window |
|---|---|---|
| Per IP Address | 5 requests | 1 minute |
| Per Team | 100 requests | 1 minute |
When rate limited, you'll receive:
HTTP Status: 429 Too Many Requests
{
"success": false,
"error": "Too many submissions from this IP. Please try again later."
}- Implement exponential backoff when retrying
- Cache form submissions client-side to prevent duplicate submissions
- Monitor your usage to stay within limits
- Contact support if you need higher limits
Contacts are matched by email or phone number within the same team:
- If a contact with the same email exists → Update contact data
- If a contact with the same phone exists → Update contact data
- Otherwise → Create new contact
Important: When updating an existing contact, their account link is preserved. The API will NOT change which account they belong to.
Accounts are matched by exact name:
- If account with exact name exists → Update account data
- Otherwise → Create new account
If no account.name is provided, a default account is created using contact.lastName.
A new deal is ALWAYS created for each form submission:
-
Deal Name: Auto-generated as
[Account/Contact Name] - [sourceName] - Account Link: Uses existing contact's account if contact was found, otherwise uses provided/created account
-
Contact Link: Added to
relatedContactsarray -
Default Stage:
s0(Lead/Prospect) - Default Closing Date: 3 months from submission date
-
Source Tracking:
sourceNameis stored for campaign attribution
When a contact already exists in the system:
- Contact data is updated (but account link preserved)
- A new deal is created
- Deal is linked to the contact's existing account
- A note is added to the deal description:
Note: Contact already existed in the system (matched by email or phone).
curl -X POST https://europe-west9-stood-abcd.cloudfunctions.net/webFormSubmit \
-H "Content-Type: application/json" \
-d '{
"teamId": "zDdmpWNC5RcXSpTeklPw",
"teamKey": "sk_live_abc123def456xyz789",
"formData": {
"contact.firstName": "John",
"contact.lastName": "Doe",
"contact.email": "john.doe@acme.com",
"contact.phone": "+1-555-123-4567",
"account.name": "Acme Corporation",
"deal.description": "Interested in enterprise solution",
"deal.amount": 50000,
"sourceName": "Website Contact Form"
}
}'const axios = require('axios');
async function submitToStoodCRM() {
try {
const response = await axios.post(
'https://europe-west9-stood-abcd.cloudfunctions.net/webFormSubmit',
{
teamId: 'zDdmpWNC5RcXSpTeklPw',
teamKey: 'sk_live_abc123def456xyz789',
formData: {
'contact.firstName': 'John',
'contact.lastName': 'Doe',
'contact.email': 'john.doe@acme.com',
'contact.phone': '+1-555-123-4567',
'account.name': 'Acme Corporation',
'deal.description': 'Interested in enterprise solution',
'deal.amount': 50000,
'sourceName': 'Website Contact Form'
}
}
);
console.log('Success:', response.data);
// {
// success: true,
// accountId: "...",
// contactId: "...",
// dealId: "...",
// isNewContact: true,
// isNewAccount: true
// }
} catch (error) {
console.error('Error:', error.response.data);
}
}
submitToStoodCRM();async function submitForm(formData) {
try {
const response = await fetch(
'https://europe-west9-stood-abcd.cloudfunctions.net/webFormSubmit',
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
teamId: 'zDdmpWNC5RcXSpTeklPw',
teamKey: 'sk_live_abc123def456xyz789',
formData: {
'contact.firstName': formData.firstName,
'contact.lastName': formData.lastName,
'contact.email': formData.email,
'contact.phone': formData.phone,
'account.name': formData.company,
'deal.description': formData.message,
'sourceName': 'Contact Form'
}
})
}
);
const result = await response.json();
if (result.success) {
console.log('✅ Submission successful!');
console.log('Deal ID:', result.dealId);
} else {
console.error('❌ Submission failed:', result.error);
}
} catch (error) {
console.error('❌ Network error:', error);
}
}import requests
import json
def submit_to_stood_crm():
url = "https://europe-west9-stood-abcd.cloudfunctions.net/webFormSubmit"
payload = {
"teamId": "zDdmpWNC5RcXSpTeklPw",
"teamKey": "sk_live_abc123def456xyz789",
"formData": {
"contact.firstName": "John",
"contact.lastName": "Doe",
"contact.email": "john.doe@acme.com",
"contact.phone": "+1-555-123-4567",
"account.name": "Acme Corporation",
"deal.description": "Interested in enterprise solution",
"deal.amount": 50000,
"sourceName": "Website Contact Form"
}
}
headers = {
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
if response.status_code == 200:
result = response.json()
print("✅ Success:", result)
print(f"Deal ID: {result['dealId']}")
else:
print("❌ Error:", response.json())
submit_to_stood_crm()<?php
function submitToStoodCRM() {
$url = "https://europe-west9-stood-abcd.cloudfunctions.net/webFormSubmit";
$data = array(
"teamId" => "zDdmpWNC5RcXSpTeklPw",
"teamKey" => "sk_live_abc123def456xyz789",
"formData" => array(
"contact.firstName" => "John",
"contact.lastName" => "Doe",
"contact.email" => "john.doe@acme.com",
"contact.phone" => "+1-555-123-4567",
"account.name" => "Acme Corporation",
"deal.description" => "Interested in enterprise solution",
"deal.amount" => 50000,
"sourceName" => "Website Contact Form"
)
);
$options = array(
'http' => array(
'header' => "Content-Type: application/json\r\n",
'method' => 'POST',
'content' => json_encode($data)
)
);
$context = stream_context_create($options);
$result = file_get_contents($url, false, $context);
if ($result === FALSE) {
echo "❌ Error submitting form\n";
} else {
$response = json_decode($result, true);
echo "✅ Success: " . print_r($response, true);
}
}
submitToStoodCRM();
?>require 'net/http'
require 'json'
require 'uri'
def submit_to_stood_crm
uri = URI('https://europe-west9-stood-abcd.cloudfunctions.net/webFormSubmit')
payload = {
teamId: 'zDdmpWNC5RcXSpTeklPw',
teamKey: 'sk_live_abc123def456xyz789',
formData: {
'contact.firstName': 'John',
'contact.lastName': 'Doe',
'contact.email': 'john.doe@acme.com',
'contact.phone': '+1-555-123-4567',
'account.name': 'Acme Corporation',
'deal.description': 'Interested in enterprise solution',
'deal.amount': 50000,
'sourceName': 'Website Contact Form'
}
}
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri.path, {'Content-Type' => 'application/json'})
request.body = payload.to_json
response = http.request(request)
result = JSON.parse(response.body)
if result['success']
puts "✅ Success: #{result}"
puts "Deal ID: #{result['dealId']}"
else
puts "❌ Error: #{result['error']}"
end
end
submit_to_stood_crm{
"info": {
"name": "Stood CRM Web Form API",
"description": "API for submitting contacts, accounts, and deals to Stood CRM",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"variable": [
{
"key": "baseUrl",
"value": "https://europe-west9-stood-abcd.cloudfunctions.net",
"type": "string"
},
{
"key": "teamId",
"value": "your-team-id",
"type": "string"
},
{
"key": "teamKey",
"value": "your-api-key",
"type": "string"
}
],
"item": [
{
"name": "Submit Web Form - Minimal",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"teamId\": \"{{teamId}}\",\n \"teamKey\": \"{{teamKey}}\",\n \"formData\": {\n \"contact.email\": \"test@example.com\",\n \"contact.firstName\": \"Test\",\n \"contact.lastName\": \"User\"\n }\n}"
},
"url": {
"raw": "{{baseUrl}}/webFormSubmit",
"host": ["{{baseUrl}}"],
"path": ["webFormSubmit"]
},
"description": "Minimal form submission with only required fields"
}
},
{
"name": "Submit Web Form - Complete",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"teamId\": \"{{teamId}}\",\n \"teamKey\": \"{{teamKey}}\",\n \"formData\": {\n \"contact.firstName\": \"John\",\n \"contact.lastName\": \"Doe\",\n \"contact.email\": \"john.doe@acme.com\",\n \"contact.phone\": \"+1-555-123-4567\",\n \"contact.role\": \"CTO\",\n \"contact.location\": \"New York, NY\",\n \n \"account.name\": \"Acme Corporation\",\n \"account.location\": \"New York, NY\",\n \"account.website\": \"https://acme.com\",\n \"account.description\": \"Leading provider of roadrunner traps\",\n \n \"deal.name\": \"Acme Corp - Enterprise Plan\",\n \"deal.amount\": 50000,\n \"deal.stage\": \"s1\",\n \"deal.description\": \"Interested in our enterprise solution for Q1 2025\",\n \"deal.solution\": \"Enterprise Plan + Premium Support\",\n \"deal.closingDate\": \"2025-03-31\",\n \n \"sourceName\": \"Website Contact Form\"\n }\n}"
},
"url": {
"raw": "{{baseUrl}}/webFormSubmit",
"host": ["{{baseUrl}}"],
"path": ["webFormSubmit"]
},
"description": "Complete form submission with all available fields"
}
},
{
"name": "Submit Web Form - With Custom Fields",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"teamId\": \"{{teamId}}\",\n \"teamKey\": \"{{teamKey}}\",\n \"formData\": {\n \"contact.email\": \"jane@startup.io\",\n \"contact.firstName\": \"Jane\",\n \"contact.lastName\": \"Smith\",\n \n \"account.name\": \"Startup Inc\",\n \n \"deal.name\": \"Startup Inc - Annual License\",\n \"deal.amount\": 25000,\n \"deal.partnerKey\": \"PARTNER-2024-001\",\n \"deal.industry\": \"SaaS\",\n \"deal.referralSource\": \"LinkedIn Campaign\",\n \n \"account.companySize\": \"50-100\",\n \"account.annualRevenue\": \"5M-10M\",\n \n \"sourceName\": \"LinkedIn Ads Q4\"\n }\n}"
},
"url": {
"raw": "{{baseUrl}}/webFormSubmit",
"host": ["{{baseUrl}}"],
"path": ["webFormSubmit"]
},
"description": "Form submission with custom fields"
}
},
{
"name": "Test Connection",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"teamId\": \"{{teamId}}\",\n \"teamKey\": \"{{teamKey}}\",\n \"formData\": {}\n}"
},
"url": {
"raw": "{{baseUrl}}/webFormSubmit",
"host": ["{{baseUrl}}"],
"path": ["webFormSubmit"]
},
"description": "Test connection with empty formData (validates credentials only)"
}
}
]
}- Copy the JSON above
- Open Postman
- Click Import → Raw text
- Paste the JSON
- Click Import
- Set your variables:
-
baseUrl: Your Cloud Functions base URL -
teamId: Your Team ID -
teamKey: Your API Key
-
-
Never expose API keys client-side
<!-- ❌ BAD: API key visible in browser --> <script> const teamKey = 'sk_live_abc123def456xyz789'; </script>
-
Use server-side proxy
Browser → Your Server → Stood CRM APIYour server holds the credentials and forwards sanitized data.
-
Rotate keys regularly
- Generate new API keys periodically
- Revoke old keys after migration
-
Monitor usage
- Check Firestore logs for suspicious activity
- Set up alerts for unusual patterns
-
Validate before submitting
// Validate email format const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { throw new Error('Invalid email'); }
-
Sanitize user input
// Remove extra whitespace const cleanEmail = email.trim().toLowerCase();
-
Use consistent formatting
// Phone numbers: use international format const phone = '+1-555-123-4567'; // Good // Not: '555.123.4567' or '(555) 123-4567'
-
Implement retry logic with exponential backoff
async function submitWithRetry(data, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { return await submit(data); } catch (error) { if (i === maxRetries - 1) throw error; await sleep(Math.pow(2, i) * 1000); // 1s, 2s, 4s } } }
-
Debounce form submissions
let submitTimeout; function debouncedSubmit(data) { clearTimeout(submitTimeout); submitTimeout = setTimeout(() => submit(data), 500); }
-
Cache to prevent duplicates
const submitted = new Set(); function submitOnce(email, data) { if (submitted.has(email)) return; submit(data); submitted.add(email); }
Error: Invalid team credentials
Causes:
- Incorrect Team ID (case-sensitive)
- Incorrect API Key
- API Key not yet generated
Solutions:
- Verify Team ID from Stood CRM Admin panel
- Regenerate API Key if lost
- Check for typos (trailing spaces, etc.)
Error: Either contact.email or contact.phone is required
Solution: Include at least one contact identifier:
{
"formData": {
"contact.email": "user@example.com"
// OR "contact.phone": "+1-555-1234"
}
}Error: At least contact.firstName, contact.lastName, or account.name is required
Solution: Provide at least one name field:
{
"formData": {
"contact.firstName": "John"
// OR "contact.lastName": "Doe"
// OR "account.name": "Acme Corp"
}
}Error: Invalid email format
Solution: Ensure email matches the pattern: name@domain.com
Error: Too many submissions from this IP
Solutions:
- Wait 1 minute before retrying
- Implement exponential backoff
- Contact support for higher limits
Error: Failed to fetch or Network error
Causes:
- Incorrect base URL
- CORS issues (browser-based requests)
- Firewall blocking requests
Solutions:
- Verify base URL matches your Firebase region
- Use server-side proxy to avoid CORS
- Check firewall/network settings
Error: Internal server error
Causes:
- Server-side error
- Malformed request
- Firestore permission issues
Solutions:
- Check Cloud Functions logs in Firebase Console
- Verify JSON is valid
- Ensure all required fields are strings (not objects/arrays unless specified)
- Contact support with error logs
- Documentation: Stood CRM Docs Wiki
- Make.com Connector: Setup Guide
- Zapier Integration: Setup Guide
-
Check Logs:
- Firebase Console → Functions → Logs
- Look for error details
-
Community:
- Check existing documentation
- Review troubleshooting section
-
Contact Support:
- Email your Stood CRM administrator
- Include:
- Error messages
- Request/response examples (remove sensitive data)
- Timestamp of the issue
- Your Team ID (not API key!)
- Initial API release
- Support for contacts, accounts, and deals
- Custom fields support
- Rate limiting
- Deduplication logic
- Source tracking
Happy Integrating! 🚀
For the latest updates, visit the Stood CRM Documentation.