Webform Inbound API

Stood CRM Web Form API Documentation

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

The Stood CRM Web Form API allows you to programmatically submit contact, account, and deal information to your Stood CRM. This is ideal for:

Key Features:


Authentication

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)

How to Get Your Credentials

1. Team ID

  1. Log in to Stood CRM

  2. Go to AdminTeam Management

  3. Your Team ID is displayed in the team card header (e.g., zDdmpWNC5RcXSpTeklPw)

  4. Click the copy icon to copy it

2. Web Form API Key (teamKey)

  1. Log in to Stood CRM

  2. Go to AdminTeam Management

  3. Click on your team card

  4. Click on the Marketing Source section (orange box with campaign icon)

  5. Click "Generate" under WebForm API Key section

  6. Copy the generated key

  7. 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.


Base URL Structure

Your base URL follows this format:

https://[REGION]-[PROJECT-ID].cloudfunctions.net

Finding Your Base URL

  1. Go to Firebase Console

  2. Select your Stood CRM project

  3. Go to BuildFunctions

  4. Click on the webFormSubmit function

  5. 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

Common Regions

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


Endpoints

Submit Web Form

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:


Request Format

Minimal Request

At minimum, you must provide either:

{
"teamId": "your-team-id",
"teamKey": "your-api-key",
"formData": {
"contact.email": "john@example.com",
"contact.firstName": "John",
"contact.lastName": "Doe"
}
}

Complete Request Example

{
"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"
}
}

Custom Fields Example

{
"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"
}
}

Standard Fields

Fields that map directly to entity properties in Firestore.

Contact Standard Fields

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

Account Standard Fields

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

Deal Standard Fields

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)

Deal Stages

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

Special Field

Field

Type

Required

Description

sourceName

string

No

Marketing source/campaign name (used in deal naming and tracking)


Custom Fields

Any field not listed as a standard field is treated as a custom field and stored in the entity's customFields object.

Field Naming Convention

Use dot notation to specify the entity and field name:

entity.fieldName

Examples:

Custom Fields Example

{
"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"
}
}

Response Format

Success Response

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

Error Response

HTTP Status: 4xx or 5xx

{
"success": false,
"error": "Error message describing what went wrong"
}

Common Error Messages

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)


Rate Limiting

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

Rate Limit Response

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."
}

Best Practices


Business Logic

Contact Deduplication

Contacts are matched by email or phone number within the same team:

  1. If a contact with the same email exists → Update contact data

  2. If a contact with the same phone exists → Update contact data

  3. 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.

Account Handling

Accounts are matched by exact name:

  1. If account with exact name exists → Update account data

  2. Otherwise → Create new account

If no account.name is provided, a default account is created using contact.lastName.

Deal Creation

A new deal is ALWAYS created for each form submission:

Contact Already Existed Behavior

When a contact already exists in the system:

  1. Contact data is updated (but account link preserved)

  2. A new deal is created

  3. Deal is linked to the contact's existing account

  4. A note is added to the deal description:

Note: Contact already existed in the system (matched by email or phone).

Code Examples

cURL

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"
}
}'

JavaScript (Node.js)

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();

JavaScript (Browser/Fetch API)

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);
}
}

Python

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

<?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();
?>

Ruby

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

Postman Collection

Import This Collection

{
"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)"
}
}
]
}

Setup Instructions

  1. Copy the JSON above

  2. Open Postman

  3. Click ImportRaw text

  4. Paste the JSON

  5. Click Import

  6. Set your variables:

    • baseUrl: Your Cloud Functions base URL

    • teamId: Your Team ID

    • teamKey: Your API Key


Best Practices

Security

  1. Never expose API keys client-side

    <!-- ❌ BAD: API key visible in browser -->
    <script>
    const teamKey = 'sk_live_abc123def456xyz789';
    </script>
  2. Use server-side proxy

    Browser → Your Server → Stood CRM API

    Your server holds the credentials and forwards sanitized data.

  3. Rotate keys regularly

    • Generate new API keys periodically

    • Revoke old keys after migration

  4. Monitor usage

    • Check Firestore logs for suspicious activity

    • Set up alerts for unusual patterns

Data Quality

  1. Validate before submitting

    // Validate email format
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(email)) {
    throw new Error('Invalid email');
    }
  2. Sanitize user input

    // Remove extra whitespace
    const cleanEmail = email.trim().toLowerCase();
  3. Use consistent formatting

    // Phone numbers: use international format
    const phone = '+1-555-123-4567'; // Good
    // Not: '555.123.4567' or '(555) 123-4567'

Performance

  1. 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
    }
    }
    }
  2. Debounce form submissions

    let submitTimeout;
    function debouncedSubmit(data) {
    clearTimeout(submitTimeout);
    submitTimeout = setTimeout(() => submit(data), 500);
    }
  3. Cache to prevent duplicates

    const submitted = new Set();
    function submitOnce(email, data) {
    if (submitted.has(email)) return;
    submit(data);
    submitted.add(email);
    }

Troubleshooting

Authentication Errors

Error: Invalid team credentials

Causes:

Solutions:

  1. Verify Team ID from Stood CRM Admin panel

  2. Regenerate API Key if lost

  3. Check for typos (trailing spaces, etc.)

Validation Errors

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

Rate Limiting

Error: Too many submissions from this IP

Solutions:

  1. Wait 1 minute before retrying

  2. Implement exponential backoff

  3. Contact support for higher limits

Network Errors

Error: Failed to fetch or Network error

Causes:

Solutions:

  1. Verify base URL matches your Firebase region

  2. Use server-side proxy to avoid CORS

  3. Check firewall/network settings

Empty Response or 500 Error

Error: Internal server error

Causes:

Solutions:

  1. Check Cloud Functions logs in Firebase Console

  2. Verify JSON is valid

  3. Ensure all required fields are strings (not objects/arrays unless specified)

  4. Contact support with error logs


Support

Resources

Getting Help

  1. Check Logs:

    • Firebase Console → Functions → Logs

    • Look for error details

  2. Community:

    • Check existing documentation

    • Review troubleshooting section

  3. 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!)


Changelog

Version 1.0 (November 2025)


Happy Integrating! 🚀

For the latest updates, visit the Stood CRM Documentation.

Published with Nuclino