All HoopAI API requests follow REST conventions. This guide covers the patterns you’ll use in every integration.
Base URL
https://services.leadconnectorhq.com
The base URL uses the LeadConnector domain. This is the API infrastructure that powers HoopAI — all data belongs to your HoopAI account.
Request structure
Every request needs the Authorization and Version headers:
curl -X GET "https://services.leadconnectorhq.com/{endpoint}" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Version: 2021-07-28" \
-H "Content-Type: application/json"
Common parameters
| Parameter | Description | Used in |
|---|
locationId | Account ID (required for most endpoints) | Query string or body |
limit | Number of results per page (default varies by endpoint) | Query string |
offset / page | Pagination cursor | Query string |
Most list endpoints return paginated results. The response includes metadata about total results and how to fetch the next page.
{
"contacts": [...],
"meta": {
"total": 342,
"currentPage": 1,
"nextPage": 2,
"startAfter": 20,
"startAfterId": "abc123"
}
}
# Page 1
GET /contacts/?locationId=LOC_ID&limit=20
# Page 2
GET /contacts/?locationId=LOC_ID&limit=20&startAfter=20
Some endpoints use cursor-based pagination with startAfterId:
# First page
GET /contacts/?locationId=LOC_ID&limit=20
# Next page (use the last item's ID)
GET /contacts/?locationId=LOC_ID&limit=20&startAfterId=abc123
Always check the meta object in the response for pagination details. If nextPage is null, you’ve reached the last page.
Filtering and searching
Many list endpoints support filtering via query parameters or a search body:
# Filter contacts by tag
GET /contacts/?locationId=LOC_ID&query=tag:lead
# Search contacts (POST)
POST /contacts/search
{
"locationId": "LOC_ID",
"filters": [
{ "field": "tags", "operator": "contains", "value": "lead" }
],
"page": 1,
"pageLimit": 20
}
Creating and updating resources
Use POST to create and PUT to update. Always send JSON bodies with the Content-Type: application/json header.
# Create a contact
curl -X POST "https://services.leadconnectorhq.com/contacts/" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Version: 2021-07-28" \
-H "Content-Type: application/json" \
-d '{
"locationId": "LOC_ID",
"firstName": "Jane",
"lastName": "Smith",
"email": "jane@example.com",
"phone": "+15551234567",
"tags": ["lead", "website"]
}'
Error handling
All errors return a consistent JSON structure:
{
"statusCode": 422,
"message": "Validation failed",
"errors": [
{ "field": "email", "message": "Invalid email format" }
]
}
| Status | Meaning | What to do |
|---|
400 | Bad request | Check request body and parameters |
401 | Unauthorized | Token missing, expired, or invalid — refresh it |
403 | Forbidden | Token lacks required scope |
404 | Not found | Resource doesn’t exist or wrong locationId |
422 | Validation error | Check the errors array for field-level details |
429 | Rate limited | Wait and retry with exponential backoff |
500 | Server error | Retry after a short delay |
Rate limits
| Scope | Limit |
|---|
| Per account | 100 requests / 10 seconds |
| Token refresh | 5 requests / minute |
Rate limit headers are included in every response:
| Header | Description |
|---|
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix timestamp when the limit resets |
When you receive a 429 response, use exponential backoff:
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
const response = await fetch(url, options);
if (response.status !== 429) return response;
const resetAt = response.headers.get('X-RateLimit-Reset');
const waitMs = resetAt
? (parseInt(resetAt) * 1000) - Date.now()
: Math.pow(2, i) * 1000;
await new Promise(resolve => setTimeout(resolve, Math.max(waitMs, 1000)));
}
throw new Error('Max retries exceeded');
}
Next steps