Version: 1.0
Last Updated: November 2025
Base URL: Same as Webform Inbound API
The Sub-Collection Items API allows you to create items in deal sub-collections directly via web form submissions. This is ideal for:
Order processing systems
Invoice management
Product catalogs
Service requests
Any structured data that belongs to a deal
Key Features:
✅ Automatic account creation if not found
✅ Automatic deal creation (s0 stage) if no active deal exists
✅ Smart deal selection (finds best active deal: s0 > s1 > s2)
✅ Creation-only (no updates to existing items)
✅ Support for custom sub-collection fields
✅ Same authentication and rate limiting as main webform API
How It Works:
Sub-collection submissions are automatically detected by the presence of subCollectionName or subCollection.* fields
The system finds or creates an account based on accountName
The system finds the best active deal (s0, s1, or s2) for that account
If no active deal exists, a new s0 deal is created
The sub-collection item is created in the selected deal
Uses the same authentication as the main Webform Inbound API:
Parameter | Type | Location | Description |
teamId | string | Body | Your Stood CRM Team ID |
teamKey | string | Body | Your Web Form API Key (webFormKey) |
See the main API documentation for details on obtaining credentials.
Endpoint: POST /webFormSubmit (same endpoint as regular webform submissions)
Full URL: https://[REGION]-[PROJECT-ID].cloudfunctions.net/webFormSubmit
Content-Type: application/json
Rate Limit: Same as main API (100 requests per minute per team)
Detection: The API automatically detects sub-collection submissions by checking for:
subCollectionName field, OR
Fields starting with subCollection.
Field | Type | Description |
| string | Account name (required to link sub-collection) |
| string | Name of the sub-collection (required) |
Sub-collection fields can be provided in two formats:
{"teamId": "your-team-id","teamKey": "your-api-key","formData": {"accountName": "Acme Corporation","subCollectionName": "orders","orderNumber": "ORD-2024-001","orderDate": "2024-11-15","totalAmount": "1250.00","status": "pending"}}
{"teamId": "your-team-id","teamKey": "your-api-key","formData": {"account.name": "Acme Corporation","subCollectionName": "orders","subCollection.orders.orderNumber": "ORD-2024-001","subCollection.orders.orderDate": "2024-11-15","subCollection.orders.totalAmount": "1250.00","subCollection.orders.status": "pending"}}
{"teamId": "zDdmpWNC5RcXSpTeklPw","teamKey": "sk_live_abc123def456xyz789","formData": {"accountName": "Acme Corporation","subCollectionName": "invoices","invoiceNumber": "INV-2024-001","invoiceDate": "2024-11-15","dueDate": "2024-12-15","amount": "5000.00","currency": "USD","status": "unpaid","description": "Q4 2024 Service Invoice","sourceName": "Accounting System"}}
To create multiple items, make separate API calls (one per item):
// Item 1await submitSubCollection({accountName: "Acme Corporation",subCollectionName: "orders",orderNumber: "ORD-001",// ... other fields});// Item 2await submitSubCollection({accountName: "Acme Corporation",subCollectionName: "orders",orderNumber: "ORD-002",// ... other fields});
Provide the account name using either format:
accountName (direct field)
account.name (dot notation)
Provide the sub-collection name using either format:
subCollectionName (direct field) - Recommended
Inferred from subCollection.{name}.{field} format
All other fields are treated as sub-collection item data:
Direct Format (Recommended):
{"subCollectionName": "orders","orderNumber": "ORD-001","orderDate": "2024-11-15"}
Dot Notation Format:
{"subCollectionName": "orders","subCollection.orders.orderNumber": "ORD-001","subCollection.orders.orderDate": "2024-11-15"}
Important Notes:
All fields are stored flat at the root level (no nested structure)
Field names are case-sensitive
Empty or whitespace-only values are ignored
Maximum field value length: 500 characters
Field values are automatically sanitized (trimmed)
{"success": true,"accountId": "abc123def456","dealId": "xyz789ghi012","subCollectionName": "orders","itemId": "item123456","isNewAccount": false,"isNewDeal": true}
Field | Type | Description |
| boolean | Always |
| string | Firestore document ID of the account |
| string | Firestore document ID of the deal where item was created |
| string | Name of the sub-collection |
| string | Firestore document ID of the created item |
| boolean |
|
| boolean |
|
{"success": false,"error": "Error message describing what went wrong"}
Find Existing Account: Searches for account by exact name match
Create New Account: If not found, creates a new account with:
Name: provided accountName
All other fields: empty/default values
createdAt: server timestamp
The system follows this priority order:
Find Active Deal: Searches for existing deals with:
Matching account ID
Matching team ID
Stage: s0, s1, or s2 (in that priority order)
Sorted by creationDate (newest first)
Create New Deal: If no active deal found, creates a new deal with:
name: {accountName} - {sourceName} or just {accountName}
account: account ID
stage: s0 (Prospect)
amount: 0
team: team ID
source: sourceName or "webform"
closingDate: 3 months from now (default)
All other fields: empty/default values
Path: deals/{dealId}/{subCollectionName}/{itemId}
Data Structure: Flat structure (all fields at root level)
Metadata: Automatically adds createdAt timestamp
No Updates: This API only creates items (no updates to existing items)
async function submitSubCollectionItem(accountName, subCollectionName, itemData) {const response = await fetch('https://[REGION]-[PROJECT-ID].cloudfunctions.net/webFormSubmit', {method: 'POST',headers: {'Content-Type': 'application/json',},body: JSON.stringify({teamId: 'your-team-id',teamKey: 'your-api-key',formData: {accountName: accountName,subCollectionName: subCollectionName,...itemData}})});const result = await response.json();if (result.success) {console.log('Item created:', result.itemId);console.log('Deal:', result.dealId);console.log('Account:', result.accountId);} else {console.error('Error:', result.error);}return result;}// Usageawait submitSubCollectionItem('Acme Corporation', 'orders', {orderNumber: 'ORD-2024-001',orderDate: '2024-11-15',totalAmount: '1250.00',status: 'pending'});
import requestsimport jsondef submit_sub_collection_item(account_name, sub_collection_name, item_data):url = 'https://[REGION]-[PROJECT-ID].cloudfunctions.net/webFormSubmit'payload = {'teamId': 'your-team-id','teamKey': 'your-api-key','formData': {'accountName': account_name,'subCollectionName': sub_collection_name,**item_data}}response = requests.post(url, json=payload)result = response.json()if result['success']:print(f"Item created: {result['itemId']}")print(f"Deal: {result['dealId']}")print(f"Account: {result['accountId']}")else:print(f"Error: {result['error']}")return result# Usagesubmit_sub_collection_item('Acme Corporation', 'orders', {'orderNumber': 'ORD-2024-001','orderDate': '2024-11-15','totalAmount': '1250.00','status': 'pending'})
curl -X POST https://[REGION]-[PROJECT-ID].cloudfunctions.net/webFormSubmit \-H "Content-Type: application/json" \-d '{"teamId": "your-team-id","teamKey": "your-api-key","formData": {"accountName": "Acme Corporation","subCollectionName": "orders","orderNumber": "ORD-2024-001","orderDate": "2024-11-15","totalAmount": "1250.00","status": "pending"}}'
Error:
{"success": false,"error": "accountName is required for sub-collection submissions (provide accountName or account.name)"}
Solution: Include accountName or account.name in your request.
Error:
{"success": false,"error": "Sub-collection name is required (provide subCollectionName or use subCollection.{name}.{field} format)"}
Solution: Include subCollectionName or use subCollection.{name}.{field} format.
Error:
{"success": false,"error": "At least one sub-collection field is required"}
Solution: Provide at least one field (besides accountName and subCollectionName).
Error:
{"success": false,"error": "Too many submissions from this IP. Please try again later."}
Solution: Wait 1 minute before retrying, or implement exponential backoff.
Same as main API - see Webform Inbound API documentation for details.
✅ Recommended:
{"accountName": "Acme Corp","subCollectionName": "orders","orderNumber": "ORD-001","orderDate": "2024-11-15"}
❌ Avoid (unless necessary):
{"account.name": "Acme Corp","subCollection.orders.orderNumber": "ORD-001"}
function validateSubCollectionData(data) {if (!data.accountName) {throw new Error('accountName is required');}if (!data.subCollectionName) {throw new Error('subCollectionName is required');}if (Object.keys(data).length <= 2) {throw new Error('At least one sub-collection field is required');}return true;}
async function submitWithErrorHandling(data) {try {const result = await submitSubCollectionItem(data);if (result.success) {// Log successconsole.log(`Created item ${result.itemId} in deal ${result.dealId}`);// Handle new account/deal creationif (result.isNewAccount) {console.log('New account created:', result.accountId);}if (result.isNewDeal) {console.log('New deal created:', result.dealId);}} else {// Handle errorconsole.error('Submission failed:', result.error);throw new Error(result.error);}} catch (error) {// Handle network errors, etc.console.error('Request failed:', error);throw error;}}
Account matching is case-sensitive and exact. Use consistent naming:
✅ Good:
const accountName = companyName.trim().toLowerCase();
❌ Bad:
// Inconsistent casing/spacing"Acme Corporation""acme corporation""Acme Corporation" // extra spaces
async function submitWithRetry(data, maxRetries = 3) {for (let i = 0; i < maxRetries; i++) {try {const result = await submitSubCollectionItem(data);if (result.success) {return result;}// If rate limited, wait before retryif (result.error.includes('rate limit')) {await sleep(Math.pow(2, i) * 1000); // Exponential backoffcontinue;}throw new Error(result.error);} catch (error) {if (i === maxRetries - 1) throw error;await sleep(Math.pow(2, i) * 1000);}}}
Use the response to track what was created:
const result = await submitSubCollectionItem(data);// Store references for later useconst itemReference = {accountId: result.accountId,dealId: result.dealId,subCollectionName: result.subCollectionName,itemId: result.itemId,createdAt: new Date()};// Save to your database for trackingawait saveItemReference(itemReference);
{"accountName": "Acme Corporation","subCollectionName": "orders","orderNumber": "ORD-2024-001","orderDate": "2024-11-15","totalAmount": "1250.00","currency": "USD","status": "pending","items": "3x Product A, 2x Product B"}
{"accountName": "Tech Startup Inc","subCollectionName": "invoices","invoiceNumber": "INV-2024-001","invoiceDate": "2024-11-15","dueDate": "2024-12-15","amount": "5000.00","currency": "USD","status": "unpaid","description": "Q4 2024 Service Invoice"}
{"accountName": "Enterprise Client","subCollectionName": "serviceRequests","requestNumber": "SR-2024-001","requestDate": "2024-11-15","priority": "high","category": "technical support","description": "Need assistance with API integration","assignedTo": "support-team"}
Feature | Regular Webform API | Sub-Collection API |
Purpose | Create contacts, accounts, deals | Create sub-collection items |
Required Fields |
|
|
Deal Creation | Always creates new deal | Only creates if no active deal exists |
Contact Handling | Creates/updates contacts | No contact handling |
Account Handling | Optional, creates from contact if needed | Required, creates if not found |
Item Updates | Updates existing contacts/accounts | Creation only (no updates) |
Possible Causes:
Wrong sub-collection name (check spelling, case-sensitive)
Deal was created but item creation failed
Firestore permissions issue
Solutions:
Verify sub-collection name matches exactly (case-sensitive)
Check API response for itemId - if present, item was created
Verify team has permissions to write to sub-collections
Possible Causes:
Multiple active deals for the account
Deal priority selection (s0 > s1 > s2) not as expected
Solutions:
Check which deal was used via response dealId
Close or move unwanted deals to s3/s4 to exclude them
Create specific deal and use regular webform API instead
Possible Causes:
Account name mismatch (case-sensitive, exact match required)
Extra spaces or special characters
Solutions:
Verify exact account name in Stood CRM
Trim and normalize account name before submission
Check for hidden characters or encoding issues
For additional help, see the main Webform Inbound API documentation or contact your Stood CRM administrator.
Initial sub-collection API release
Support for automatic account creation
Support for automatic deal creation/selection
Creation-only mode (no updates)
Smart deal selection (s0 > s1 > s2 priority)
Happy Integrating! 🚀