The Generic REST API provides a unified, resource-based interface for managing all Stood resources. It follows REST best practices with standard HTTP methods and resource-based URLs.
All API requests require authentication using your team's webFormKey. API keys must be passed in headers for security.
Required Headers:
X-Team-ID: Your team ID
Authorization: Bearer {webFormKey}: Your webFormKey
Example:
X-Team-ID: abc123Authorization: Bearer webform_xyz789
Security Note: API keys in headers are more secure than in URLs (which can be logged, cached, or exposed in browser history).
Log in to your Stood admin console
Navigate to Team Settings
Find your webFormKey (also called "Web Form API Key")
Use this key in all API requests
https://[region]-[project-id].cloudfunctions.net/api/v1
Replace [region] with your Firebase Functions region (e.g., us-central1) and [project-id] with your Firebase project ID.
accounts - Account management
contacts - Contact management
deals - Deal management
posts - Post/comment management
activities - Activity management
folders - Folder management (nested under deals)
{subCollectionName} - Custom sub-collections (nested under deals, defined in your custom datamodel settings)
GET /api/v1/{resource}?[filters]&limit=100&offset=0Headers:X-Team-ID: {teamId}Authorization: Bearer {webFormKey}
Headers (Required):
X-Team-ID: Your team ID
Authorization: Bearer {webFormKey}: Your webFormKey
Query Parameters:
limit (optional) - Max results (default: 100, max: 1000)
offset (optional) - Pagination offset (default: 0). Note: Large offsets are inefficient; consider using search to narrow results instead.
search or q (optional) - Search term for name matching (case-insensitive). Works for:
accounts: Searches in name field
contacts: Searches in firstName and lastName fields
deals: Searches in name field
Additional filters - Any field from the resource can be used as an exact match filter
Examples:
List deals with stage filter:
GET /api/v1/deals?stage=s0&limit=50Headers:X-Team-ID: abc123Authorization: Bearer webform_xyz
Search accounts by name:
GET /api/v1/accounts?search=acme&limit=20Headers:X-Team-ID: abc123Authorization: Bearer webform_xyz
Search contacts by name with pagination:
GET /api/v1/contacts?q=john&limit=50&offset=0Headers:X-Team-ID: abc123Authorization: Bearer webform_xyz
Response:
{"success": true,"data": [{"id": "deal-id","name": "Deal Name","stage": "s0","team": "team-id",...}],"count": 10,"total": 10}
GET /api/v1/{resource}/{id}Headers:X-Team-ID: {teamId}Authorization: Bearer {webFormKey}
Example:
GET /api/v1/deals/deal123Headers:X-Team-ID: abc123Authorization: Bearer webform_xyz
Response:
{"success": true,"data": {"id": "deal-id","name": "Deal Name",...}}
POST /api/v1/{resource}Headers:X-Team-ID: {teamId}Authorization: Bearer {webFormKey}Content-Type: application/json{"name": "...",...}
Example:
POST /api/v1/accountsHeaders:X-Team-ID: abc123Authorization: Bearer webform_xyzContent-Type: application/jsonBody:{"name": "Acme Corp","location": "New York","website": "https://acme.com"}
Response:
{"success": true,"data": {"id": "account-id","name": "Acme Corp","createdAt": "2024-01-15T10:30:00.000Z",...}}
PUT /api/v1/{resource}/{id}Headers:X-Team-ID: {teamId}Authorization: Bearer {webFormKey}Content-Type: application/json{"name": "Updated Name",...}
Note: Only include fields you want to update. The team and id fields cannot be updated.
Example:
PUT /api/v1/deals/deal123Headers:X-Team-ID: abc123Authorization: Bearer webform_xyzContent-Type: application/jsonBody:{"name": "Updated Deal Name","stage": "s1"}
Response:
{"success": true,"data": {"id": "deal-id","name": "Updated Deal Name","stage": "s1","updatedAt": "2024-01-15T11:00:00.000Z",...}}
DELETE /api/v1/{resource}/{id}Headers:X-Team-ID: {teamId}Authorization: Bearer {webFormKey}
Example:
DELETE /api/v1/accounts/account123Headers:X-Team-ID: abc123Authorization: Bearer webform_xyz
Response:
{"success": true,"message": "Resource deleted successfully"}
Nested resources are accessed via their parent resource path.
GET /api/v1/{parent}/{parentId}/{nested}?[filters]Headers:X-Team-ID: {teamId}Authorization: Bearer {webFormKey}
Example - List folders for a deal:
GET /api/v1/deals/deal123/foldersHeaders:X-Team-ID: abc123Authorization: Bearer webform_xyz
Response:
{"success": true,"data": [{"id": "folder-id","dealId": "deal123","name": "Dropbox","url": "https://dropbox.com/folder","preview": "# Summary...","createdAt": "2024-01-15T10:30:00.000Z"}],"count": 1}
GET /api/v1/{parent}/{parentId}/{nested}/{nestedId}Headers:X-Team-ID: {teamId}Authorization: Bearer {webFormKey}
Example:
GET /api/v1/deals/deal123/folders/folder456Headers:X-Team-ID: abc123Authorization: Bearer webform_xyz
POST /api/v1/{parent}/{parentId}/{nested}Headers:X-Team-ID: {teamId}Authorization: Bearer {webFormKey}Content-Type: application/json{"name": "...",...}
Example - Create folder:
POST /api/v1/deals/deal123/foldersHeaders:X-Team-ID: abc123Authorization: Bearer webform_xyzContent-Type: application/jsonBody:{"name": "Dropbox","url": "https://dropbox.com/folder","preview": "# Summary\n\nImportant documents."}
PUT /api/v1/{parent}/{parentId}/{nested}/{nestedId}Headers:X-Team-ID: {teamId}Authorization: Bearer {webFormKey}Content-Type: application/json{"name": "Updated Name",...}
DELETE /api/v1/{parent}/{parentId}/{nested}/{nestedId}Headers:X-Team-ID: {teamId}Authorization: Bearer {webFormKey}
For nested resources like folders, you can access them directly by ID without knowing the parent deal ID. The API will search for the resource across all deals in your team.
Note: This is less efficient than using the full path, but useful when you only have the nested resource ID.
GET /api/v1/folders/{folderId}Headers:X-Team-ID: {teamId}Authorization: Bearer {webFormKey}
Example:
GET /api/v1/folders/folder456Headers:X-Team-ID: abc123Authorization: Bearer webform_xyz
Response:
{"success": true,"data": {"id": "folder-id","dealId": "deal123","name": "Dropbox",...},"parentId": "deal123","parentCollection": "deals"}
Required fields (create):
name (string)
Optional fields:
location (string)
website (string)
description (string)
parent (string) - Parent account ID
archived (boolean)
customFields (object) - Custom field values
Required fields (create):
firstName (string)
lastName (string)
Optional fields:
email (string)
phone (string)
role (string)
location (string)
account (string) - Account ID
archived (boolean)
customFields (object)
Required fields (create):
name (string)
accountId (string)
stage (string) - One of: s0, s1, s2, s3, s4
Optional fields:
amount (number)
description (string)
solution (string)
closingDate (ISO 8601 string)
relatedContacts (array of contact IDs)
tags (array of strings)
owner (string) - User ID
parent (string) - Parent deal ID
customFields (object)
Required fields (create):
content (string)
deal (string) - Deal ID
author (string) - User ID
Optional fields:
docTitle (string)
docContent (string) - Markdown content
comments (array)
generated (boolean)
Required fields (create):
title (string)
deal (string) - Deal ID
owner (string) - User ID
Optional fields:
description (string)
date (ISO 8601 string)
status (string) - One of: open, done, cancelled
customFields (object)
Required fields (create):
name (string)
Optional fields:
url (string) - Must be valid HTTP/HTTPS URL
preview (string) - Markdown formatted summary
createdBy (string) - User ID
Custom sub-collections are defined in datamodel.json under deal.subCollections. They can be accessed the same way as folders:
GET /api/v1/deals/{dealId}/{subCollectionName}POST /api/v1/deals/{dealId}/{subCollectionName}GET /api/v1/deals/{dealId}/{subCollectionName}/{itemId}PUT /api/v1/deals/{dealId}/{subCollectionName}/{itemId}DELETE /api/v1/deals/{dealId}/{subCollectionName}/{itemId}
All errors follow this format:
{"success": false,"error": "Error message description"}
HTTP Status Codes:
200 - Success (GET, PUT, DELETE)
201 - Created (POST)
400 - Bad Request (validation errors, missing fields)
401 - Unauthorized (invalid credentials)
404 - Not Found (resource doesn't exist)
405 - Method Not Allowed (wrong HTTP method)
500 - Internal Server Error
List all deals:
curl "https://us-central1-your-project.cloudfunctions.net/api/v1/deals" \-H "X-Team-ID: abc123" \-H "Authorization: Bearer webform_xyz"
Get a specific deal:
curl "https://us-central1-your-project.cloudfunctions.net/api/v1/deals/deal123" \-H "X-Team-ID: abc123" \-H "Authorization: Bearer webform_xyz"
Create an account:
curl -X POST https://us-central1-your-project.cloudfunctions.net/api/v1/accounts \-H "X-Team-ID: abc123" \-H "Authorization: Bearer webform_xyz" \-H "Content-Type: application/json" \-d '{"name": "Acme Corp","location": "New York"}'
Update a deal:
curl -X PUT https://us-central1-your-project.cloudfunctions.net/api/v1/deals/deal123 \-H "X-Team-ID: abc123" \-H "Authorization: Bearer webform_xyz" \-H "Content-Type: application/json" \-d '{"stage": "s1"}'
List folders for a deal:
curl "https://us-central1-your-project.cloudfunctions.net/api/v1/deals/deal123/folders" \-H "X-Team-ID: abc123" \-H "Authorization: Bearer webform_xyz"
Create a folder:
curl -X POST https://us-central1-your-project.cloudfunctions.net/api/v1/deals/deal123/folders \-H "X-Team-ID: abc123" \-H "Authorization: Bearer webform_xyz" \-H "Content-Type: application/json" \-d '{"name": "Dropbox","url": "https://dropbox.com/folder"}'
Get folder directly by ID:
curl "https://us-central1-your-project.cloudfunctions.net/api/v1/folders/folder456" \-H "X-Team-ID: abc123" \-H "Authorization: Bearer webform_xyz"
const baseUrl = 'https://us-central1-your-project.cloudfunctions.net/api/v1';const teamId = 'abc123';const teamKey = 'webform_xyz';// List dealsasync function listDeals() {const response = await fetch(`${baseUrl}/deals`, {headers: {'X-Team-ID': teamId,'Authorization': `Bearer ${teamKey}`}});return await response.json();}// Create accountasync function createAccount(name) {const response = await fetch(`${baseUrl}/accounts`, {method: 'POST',headers: {'X-Team-ID': teamId,'Authorization': `Bearer ${teamKey}`,'Content-Type': 'application/json'},body: JSON.stringify({ name })});return await response.json();}// Update dealasync function updateDeal(dealId, updates) {const response = await fetch(`${baseUrl}/deals/${dealId}`, {method: 'PUT',headers: {'X-Team-ID': teamId,'Authorization': `Bearer ${teamKey}`,'Content-Type': 'application/json'},body: JSON.stringify(updates)});return await response.json();}// List folders for dealasync function listFolders(dealId) {const response = await fetch(`${baseUrl}/deals/${dealId}/folders`, {headers: {'X-Team-ID': teamId,'Authorization': `Bearer ${teamKey}`}});return await response.json();}
import requestsbase_url = 'https://us-central1-your-project.cloudfunctions.net/api/v1'team_id = 'abc123'team_key = 'webform_xyz'# List dealsdef list_deals():response = requests.get(f'{base_url}/deals',headers={'X-Team-ID': team_id,'Authorization': f'Bearer {team_key}'})return response.json()# Create accountdef create_account(name):response = requests.post(f'{base_url}/accounts',headers={'X-Team-ID': team_id,'Authorization': f'Bearer {team_key}','Content-Type': 'application/json'},json={'name': name})return response.json()# Update dealdef update_deal(deal_id, updates):response = requests.put(f'{base_url}/deals/{deal_id}',headers={'X-Team-ID': team_id,'Authorization': f'Bearer {team_key}','Content-Type': 'application/json'},json=updates)return response.json()
Use appropriate HTTP methods: GET for reading, POST for creating, PUT for updating, DELETE for deleting
Include only changed fields in updates: PUT requests should only include fields you want to update
Use pagination for large lists: Use limit and offset parameters when listing resources
Filter at the API level: Use query parameters to filter results rather than fetching all and filtering client-side
Handle errors gracefully: Check the success field in responses and handle errors appropriately
Store credentials securely: Never expose teamKey in client-side code or public repositories
Currently, there are no rate limits on API operations. However, we recommend:
Implementing client-side rate limiting for high-frequency operations
Batching operations when possible
Contacting support if you need higher limits
API Key Security: Keep your webFormKey secure. Never expose it in client-side code or public repositories.
Team Isolation: Resources can only be accessed for your team. Cross-team access is not possible.
Input Validation: All inputs are sanitized and validated. Invalid data will return 400 errors.
HTTPS Only: All API calls must use HTTPS.
For issues or questions:
Check the error message in the API response
Verify your teamId and teamKey are correct
Ensure resources belong to your team
Contact support if problems persist