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
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 |
| string | Body | Your Stood CRM Team ID |
| 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 webFormSubmit function
Extract the region and project ID from the function URL
Example:
Function URL: https://webformsubmit-ccffslccvq-od.a.run.appProject ID: stood-abcdRegion: europe-west9Base URL: https://europe-west9-stood-abcd.cloudfunctions.net
Region | Location |
| Iowa, USA |
| South Carolina, USA |
| Belgium |
| London, UK |
| Paris, France |
| Taiwan |
| 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.email OR contact.phone
AND at least one of: contact.firstName, contact.lastName, OR account.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 |
| string | Conditional* | Contact's first name (max 500 chars) |
| string | Conditional* | Contact's last name (max 500 chars) |
| string | Conditional** | Valid email address |
| string | Conditional** | Phone number (any format) |
| string | No | Job title or role |
| 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 |
| string | Conditional* | Account/company name (max 500 chars) |
| string | No | Company location |
| string | No | Company website URL |
| string | No | Account description or notes (max 500 chars) |
| string | No | Parent account ID (for subsidiaries) |
* Required if no contact.lastName provided
Field | Type | Required | Description |
| string | No | Deal name (auto-generated if not provided) |
| number | No | Deal value in team's currency (default: 0) |
| string | No | Deal stage: |
| string | No | Deal description or notes (max 500 chars) |
| string | No | Proposed solution (max 500 chars) |
| string | No | Expected close date (ISO 8601 or YYYY-MM-DD format) |
| array | No | Array of tag strings |
| string | No | User ID of the deal owner |
| string | No | Parent deal ID (for sub-deals) |
Stage | Label | Description |
| Lead/Prospect | Initial contact (default) |
| Qualified | Qualified opportunity |
| Proposal | Proposal sent |
| Won | Deal closed successfully |
| Lost | Deal closed unsuccessfully |
Field | Type | Required | Description |
| 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 |
| boolean | Always |
| string | ID of created/updated account (may be empty) |
| string | ID of created/updated contact |
| string | ID of newly created deal |
| boolean |
|
| boolean |
|
HTTP Status: 4xx or 5xx
{
"success": false,
"error": "Error message describing what went wrong"
}HTTP Status | Error Message | Cause |
| Invalid team credentials | Incorrect |
| Either contact.email or contact.phone is required | Missing required contact identifier |
| At least contact.firstName, contact.lastName, or account.name is required | Missing required name field |
| Invalid email format | Email doesn't match validation regex |
| Too many submissions from this IP | Rate limit exceeded (IP) |
| Too many submissions for this team | Rate limit exceeded (team) |
| 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 relatedContacts array
Default Stage: s0 (Lead/Prospect)
Default Closing Date: 3 months from submission date
Source Tracking: sourceName is 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 API
Your 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.