Subcollection Inbound API

Stood CRM Web Form API - Sub-Collection Items

Version: 1.0
Last Updated: November 2025
Base URL: Same as Webform Inbound API


Overview

The Sub-Collection Items API allows you to create items in deal sub-collections directly via web form submissions. This is ideal for:

Key Features:

How It Works:

  1. Sub-collection submissions are automatically detected by the presence of subCollectionName or subCollection.* fields

  2. The system finds or creates an account based on accountName

  3. The system finds the best active deal (s0, s1, or s2) for that account

  4. If no active deal exists, a new s0 deal is created

  5. The sub-collection item is created in the selected deal


Authentication

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

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:


Request Format

Required Fields

Field

Type

Description

accountName or account.name

string

Account name (required to link sub-collection)

subCollectionName

string

Name of the sub-collection (required)

Field Formats

Sub-collection fields can be provided in two formats:

Format 1: Direct Field Names (Recommended)

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

Format 2: Dot Notation

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

Complete Request Example

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

Multiple Items Example

To create multiple items, make separate API calls (one per item):

// Item 1
await submitSubCollection({
accountName: "Acme Corporation",
subCollectionName: "orders",
orderNumber: "ORD-001",
// ... other fields
});
// Item 2
await submitSubCollection({
accountName: "Acme Corporation",
subCollectionName: "orders",
orderNumber: "ORD-002",
// ... other fields
});

Field Naming Convention

Account Name

Provide the account name using either format:

Sub-Collection Name

Provide the sub-collection name using either format:

Sub-Collection Fields

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:


Response Format

Success Response

{
"success": true,
"accountId": "abc123def456",
"dealId": "xyz789ghi012",
"subCollectionName": "orders",
"itemId": "item123456",
"isNewAccount": false,
"isNewDeal": true
}

Response Fields

Field

Type

Description

success

boolean

Always true for successful submissions

accountId

string

Firestore document ID of the account

dealId

string

Firestore document ID of the deal where item was created

subCollectionName

string

Name of the sub-collection

itemId

string

Firestore document ID of the created item

isNewAccount

boolean

true if account was created, false if existing

isNewDeal

boolean

true if deal was created, false if existing deal was used

Error Response

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

Business Logic

Account Resolution

  1. Find Existing Account: Searches for account by exact name match

  2. Create New Account: If not found, creates a new account with:

    • Name: provided accountName

    • All other fields: empty/default values

    • createdAt: server timestamp

Deal Resolution

The system follows this priority order:

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

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

Sub-Collection Item Creation

  1. Path: deals/{dealId}/{subCollectionName}/{itemId}

  2. Data Structure: Flat structure (all fields at root level)

  3. Metadata: Automatically adds createdAt timestamp

  4. No Updates: This API only creates items (no updates to existing items)


Code Examples

JavaScript/Node.js

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;
}
// Usage
await submitSubCollectionItem('Acme Corporation', 'orders', {
orderNumber: 'ORD-2024-001',
orderDate: '2024-11-15',
totalAmount: '1250.00',
status: 'pending'
});

Python

import requests
import json
def 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
# Usage
submit_sub_collection_item('Acme Corporation', 'orders', {
'orderNumber': 'ORD-2024-001',
'orderDate': '2024-11-15',
'totalAmount': '1250.00',
'status': 'pending'
})

cURL

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 Handling

Common Errors

Missing Account Name

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.

Missing Sub-Collection Name

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.

No Sub-Collection Fields

Error:

{
"success": false,
"error": "At least one sub-collection field is required"
}

Solution: Provide at least one field (besides accountName and subCollectionName).

Rate Limiting

Error:

{
"success": false,
"error": "Too many submissions from this IP. Please try again later."
}

Solution: Wait 1 minute before retrying, or implement exponential backoff.

Authentication Errors

Same as main API - see Webform Inbound API documentation for details.


Best Practices

1. Use Direct Field Names

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

2. Validate Before Submitting

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

3. Handle Responses Properly

async function submitWithErrorHandling(data) {
try {
const result = await submitSubCollectionItem(data);
if (result.success) {
// Log success
console.log(`Created item ${result.itemId} in deal ${result.dealId}`);
// Handle new account/deal creation
if (result.isNewAccount) {
console.log('New account created:', result.accountId);
}
if (result.isNewDeal) {
console.log('New deal created:', result.dealId);
}
} else {
// Handle error
console.error('Submission failed:', result.error);
throw new Error(result.error);
}
} catch (error) {
// Handle network errors, etc.
console.error('Request failed:', error);
throw error;
}
}

4. Use Consistent Account Names

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

5. Implement Retry Logic

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 retry
if (result.error.includes('rate limit')) {
await sleep(Math.pow(2, i) * 1000); // Exponential backoff
continue;
}
throw new Error(result.error);
} catch (error) {
if (i === maxRetries - 1) throw error;
await sleep(Math.pow(2, i) * 1000);
}
}
}

6. Monitor Created Resources

Use the response to track what was created:

const result = await submitSubCollectionItem(data);
// Store references for later use
const itemReference = {
accountId: result.accountId,
dealId: result.dealId,
subCollectionName: result.subCollectionName,
itemId: result.itemId,
createdAt: new Date()
};
// Save to your database for tracking
await saveItemReference(itemReference);

Use Cases

Order Management

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

Invoice Tracking

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

Service Requests

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

Differences from Regular Webform API

Feature

Regular Webform API

Sub-Collection API

Purpose

Create contacts, accounts, deals

Create sub-collection items

Required Fields

contact.email or contact.phone

accountName, subCollectionName

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)


Troubleshooting

Item Not Appearing in Deal

Possible Causes:

  1. Wrong sub-collection name (check spelling, case-sensitive)

  2. Deal was created but item creation failed

  3. Firestore permissions issue

Solutions:

  1. Verify sub-collection name matches exactly (case-sensitive)

  2. Check API response for itemId - if present, item was created

  3. Verify team has permissions to write to sub-collections

Wrong Deal Selected

Possible Causes:

  1. Multiple active deals for the account

  2. Deal priority selection (s0 > s1 > s2) not as expected

Solutions:

  1. Check which deal was used via response dealId

  2. Close or move unwanted deals to s3/s4 to exclude them

  3. Create specific deal and use regular webform API instead

Account Not Found (But Should Exist)

Possible Causes:

  1. Account name mismatch (case-sensitive, exact match required)

  2. Extra spaces or special characters

Solutions:

  1. Verify exact account name in Stood CRM

  2. Trim and normalize account name before submission

  3. Check for hidden characters or encoding issues


Support

For additional help, see the main Webform Inbound API documentation or contact your Stood CRM administrator.


Changelog

Version 1.0 (November 2025)


Happy Integrating! 🚀

Published with Nuclino