API Documentation
Core tenant APIs and reporting endpoints for SalesGOAT.
Base URL: https://api.salesgoat.app/api/v1
Health
Unauthenticated liveness check.
GET /health
Sample request
curl -sS -X GET "https://api.salesgoat.app/api/v1/health"
Sample response
{
"status": "healthy",
"service": "SalesGOAT API",
"environment": "production",
"projectId": "reps-e6cf4",
"timestamp": "2026-09-18T12:00:00.000Z",
"version": "1.0.0"
}
Authentication
Use a Firebase ID token for a user that belongs to the target tenant. Tokens are not minted from the master/platform tenant.
Interactive / one-off session
Sample request
curl -sS -X POST "https://api.salesgoat.app/api/v1/auth/token" \
-H "Content-Type: application/json" \
-d '{
"email": "api-USER@example.com",
"password": "YOUR_PASSWORD",
"tenantId": "YOUR_TENANT_ID"
}'
Sample response
{
"success": true,
"message": "Token generated successfully",
"data": {
"idToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "AMf-vBy...",
"userId": "firebaseAuthUid",
"tenantId": "YOUR_TENANT_ID",
"expiresIn": 3600,
"tokenType": "session"
}
}
Use the session token as:
Authorization: Bearer <idToken>
Unattended / nightly jobs (recommended)
For a job that runs on its own, use a long-lived api_user custom token
and exchange it for a session ID token once per run. That is the recommended flow.
1. Mint the long-lived API user token (store securely; do this rarely).
2. Per job run — exchange for a ~1 hour session token (shown below).
Sample request
# Mint long-lived api_user token (rarely):
curl -sS -X POST "https://api.salesgoat.app/api/v1/auth/token" \
-H "Content-Type: application/json" \
-d '{
"email": "api-USER@example.com",
"password": "YOUR_PASSWORD",
"tenantId": "YOUR_TENANT_ID",
"tokenType": "api_user"
}'
# Per job — exchange for a ~1 hour session token:
curl -sS -X POST "https://api.salesgoat.app/api/v1/auth/token" \
-H "Authorization: Bearer YOUR_API_USER_CUSTOM_TOKEN"
# equivalent:
curl -sS -X POST "https://api.salesgoat.app/api/v1/auth/session-token" \
-H "Authorization: Bearer YOUR_API_USER_CUSTOM_TOKEN"
Sample response
{
"success": true,
"message": "API user token generated successfully",
"data": {
"customToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"userId": "firebaseAuthUid",
"tenantId": "YOUR_TENANT_ID",
"expiresIn": null,
"tokenType": "api_user"
}
}
# Session exchange response:
{
"success": true,
"message": "Token generated successfully",
"data": {
"idToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "AMf-vBy...",
"userId": "firebaseAuthUid",
"tenantId": "YOUR_TENANT_ID",
"expiresIn": 3600,
"tokenType": "session"
}
}
Mint response uses expiresIn: null (long-lived); user must have api_user: true.
Use session data.idToken for authenticated calls. Re-exchange if a run exceeds about an hour.
Rate guidance: There is no application-level rate limit on these routes today. Recommended: one session exchange per nightly job, then paginated GETs. Nightly once-per-tenant exchange volume is fine; avoid hammering token exchange in a tight loop.
Errors
| Status | Meaning |
|---|---|
| 400 | Missing/invalid dates, range too large, bad query |
| 401 | Missing or invalid credentials / Bearer token |
| 403 | User not in tenant (or endpoint not allowed) |
| 404 | Resource not found |
| 500 | Server error |
Pay periods
Pay period windows used with paystub overview totals. No SCPF / line-item detail in v1.
GET /tenants/{tenantId}/pay_periods
| Param | Required | Notes |
|---|---|---|
limit | No | Default 50, max 100 |
offset | No | Default 0 |
Sample request
curl -sS -X GET \ "https://api.salesgoat.app/api/v1/tenants/YOUR_TENANT_ID/pay_periods?limit=50" \ -H "Authorization: Bearer YOUR_ID_TOKEN"
Sample response
{
"success": true,
"message": "Pay periods retrieved successfully",
"data": [
{
"id": "000045",
"tenant_id": "YOUR_TENANT_ID",
"title": "Biweekly Sep 1–14 2026",
"pay_date": "2026-09-18",
"start_date": "2026-09-01",
"end_date": "2026-09-14",
"cutoff_date": "2026-09-15",
"status": "active",
"related_payroll_groups": ["pg_hourly"],
"related_payroll_group_names": ["Hourly"],
"related_selling_season": "season_2026",
"related_selling_season_name": "2026 Season",
"is_active": true,
"created_at": "2026-08-20T15:00:00.000Z",
"updated_at": "2026-08-20T15:00:00.000Z",
"created_by": "managerUid",
"updated_by": "managerUid"
}
]
}
Paystub overview
Per-rep paystub overview totals for a pay date range or pay period. No SCPF / line-item detail in v1.
GET /tenants/{tenantId}/paystub_overview
| Param | Required | Notes |
|---|---|---|
start / end | No | Filter on pay_date (YYYY-MM-DD); provide both or neither |
pay_period_id | No | Exact pay period id |
salesProfessionalId | No | Single rep |
limit | No | Default 50, max 500 |
offset | No | Default 0 |
Useful fields for accumulation: sales_professional_id,
sales_professional_name, pay_date,
pay_period_id, total_this_period (typically cents),
office, team_ids, team_names,
area_totals. Name / office / team fields are enriched from the worker profile.
Approval / QuickBooks sync fields may also be present.
Sample request
# By pay date range: curl -sS -X GET \ "https://api.salesgoat.app/api/v1/tenants/YOUR_TENANT_ID/paystub_overview?start=2026-09-01&end=2026-09-30&limit=100" \ -H "Authorization: Bearer YOUR_ID_TOKEN" # By pay period: curl -sS -X GET \ "https://api.salesgoat.app/api/v1/tenants/YOUR_TENANT_ID/paystub_overview?pay_period_id=000045" \ -H "Authorization: Bearer YOUR_ID_TOKEN"
Sample response
{
"success": true,
"message": "Paystub overviews retrieved successfully",
"data": [
{
"id": "000210",
"tenant_id": "YOUR_TENANT_ID",
"sales_professional_id": "abcAuthUidOrPayrollId",
"sales_professional_name": "Jane Tech",
"pay_date": "2026-09-18",
"pay_period_id": "000045",
"total_this_period": 185000,
"office": "Raleigh",
"team_ids": ["team_raleigh_id"],
"team_names": ["Raleigh"],
"area_totals": {
"Raleigh": 185000
},
"approval_status": "approved",
"is_active": true,
"created_at": "2026-09-16T18:00:00.000Z",
"updated_at": "2026-09-17T12:00:00.000Z"
},
{
"id": "000211",
"tenant_id": "YOUR_TENANT_ID",
"sales_professional_id": "defAuthUidOrPayrollId",
"sales_professional_name": "Sam Driver",
"pay_date": "2026-09-18",
"pay_period_id": "000045",
"total_this_period": 92000,
"office": "Arizona",
"team_ids": ["team_arizona_id"],
"team_names": ["Arizona"],
"area_totals": {
"Arizona": 92000
},
"approval_status": "pending",
"is_active": true,
"created_at": "2026-09-16T18:05:00.000Z",
"updated_at": "2026-09-16T18:05:00.000Z"
}
]
}
Project overviews
Closed sales / project summary rows (customer, status, reps, commissions metadata).
List project overviews
GET /tenants/{tenantId}/project_overviews
| Param | Required | Notes |
|---|---|---|
salesProfessionalId | No | Filter by rep |
projectStatus | No | Exact status string |
limit | No | Default 50 |
offset | No | Default 0 |
Sample request
curl -sS -X GET \ "https://api.salesgoat.app/api/v1/tenants/YOUR_TENANT_ID/project_overviews?limit=50" \ -H "Authorization: Bearer YOUR_ID_TOKEN"
Sample response
{
"success": true,
"message": "Project overviews retrieved successfully",
"data": [
{
"id": "000501",
"tenant_id": "YOUR_TENANT_ID",
"customer_name": "Acme Residence",
"lead_token": "LEAD-99881",
"closed_date": "2026-09-10T00:00:00.000Z",
"project_status": "Closed",
"project_type": "Termite",
"related_office": "office_raleigh",
"office_name": "Raleigh",
"rep_ids": ["abcAuthUidOrPayrollId"],
"manager_ids": ["managerUid"],
"self_gen": false,
"is_active": true,
"created_by": "managerUid",
"created_by_name": "Ops Manager",
"updated_by": "managerUid",
"updated_by_name": "Ops Manager",
"created_at": "2026-09-10T16:00:00.000Z",
"updated_at": "2026-09-11T09:00:00.000Z"
}
]
}
Get project overview
GET /tenants/{tenantId}/project_overviews/{id}
Sample request
curl -sS -X GET \ "https://api.salesgoat.app/api/v1/tenants/YOUR_TENANT_ID/project_overviews/000501" \ -H "Authorization: Bearer YOUR_ID_TOKEN"
Sample response
{
"success": true,
"message": "Project overview retrieved successfully",
"data": {
"id": "000501",
"tenant_id": "YOUR_TENANT_ID",
"customer_name": "Acme Residence",
"lead_token": "LEAD-99881",
"closed_date": "2026-09-10T00:00:00.000Z",
"project_status": "Closed",
"project_type": "Termite",
"related_office": "office_raleigh",
"office_name": "Raleigh",
"rep_ids": ["abcAuthUidOrPayrollId"],
"manager_ids": ["managerUid"],
"self_gen": false,
"is_active": true,
"created_by": "managerUid",
"created_by_name": "Ops Manager",
"updated_by": "managerUid",
"updated_by_name": "Ops Manager",
"created_at": "2026-09-10T16:00:00.000Z",
"updated_at": "2026-09-11T09:00:00.000Z"
}
}
Time entries
GET /tenants/{tenantId}/time_entries?start=YYYY-MM-DD&end=YYYY-MM-DD
| Param | Required | Notes |
|---|---|---|
start / end | Yes | Inclusive local work dates; max 31 days |
limit | No | Default 500, max 1000 |
offset | No | Default 0 |
- Soft-deleted punches are omitted. Open punches return
clock_out: null. - User:
user_id+user_name(full name from the worker profile). - Clocks:
clock_in,clock_out(ISO-8601 UTC),total_hours. - Office / team:
officeis the worker profile string (e.g. Raleigh).team_ids/team_namesare the structured identifiers — use these when reporting Raleigh vs Arizona separately. FieldRoutes stops are not included; join those in your own systems. - Overtime: use
pay_type === "Overtime"(same values as the dashboard: Regular, Overtime, Sick, PTO, UTO). There is no separate overtime boolean. - Approval:
approval_statusispending|approved|rejected(same as the dashboard). - Manual edits:
edit_historyis an append-only list of dashboard/manual clock adjustments. Each item stores the previous clock-in/out/hours plusedited_by/edited_by_name. Current times remain on the top-level fields. Newest history entries are last. Empty array if never manually adjusted. System auto-close / OT reconcile do not append here. Last editor is also onupdated_by/updated_by_name.
Sample request (single day)
curl -sS -X GET \ "https://api.salesgoat.app/api/v1/tenants/YOUR_TENANT_ID/time_entries?start=2026-09-01&end=2026-09-01" \ -H "Authorization: Bearer YOUR_ID_TOKEN"
Sample response (single day)
{
"success": true,
"message": "Time entries retrieved successfully",
"data": {
"start": "2026-09-01",
"end": "2026-09-01",
"total_matched": 2,
"limit": 500,
"offset": 0,
"entries": [
{
"id": "000123",
"user_id": "abcAuthUidOrPayrollId",
"subject_auth_uid": "abcAuthUidOrPayrollId",
"user_name": "Jane Tech",
"office": "Raleigh",
"team_names": ["Raleigh"],
"team_ids": ["team_raleigh_id"],
"clock_in": "2026-09-01T12:05:00.000Z",
"clock_out": "2026-09-01T20:10:00.000Z",
"total_hours": 8.0833,
"work_date_local": "2026-09-01",
"worker_timezone": "America/New_York",
"pay_type": "Regular",
"is_open": false,
"auto_closed": false,
"approval_status": "approved",
"notes": null,
"edit_history": [],
"created_by": "abcAuthUidOrPayrollId",
"created_by_name": "Jane Tech",
"updated_by": "managerUid",
"updated_by_name": "Ops Manager",
"updated_at": "2026-09-01T20:11:00.000Z",
"created_at": "2026-09-01T12:05:00.000Z"
},
{
"id": "000124",
"user_id": "defAuthUidOrPayrollId",
"subject_auth_uid": "defAuthUidOrPayrollId",
"user_name": "Sam Driver",
"office": "Arizona",
"team_names": ["Arizona"],
"team_ids": ["team_arizona_id"],
"clock_in": "2026-09-01T14:00:00.000Z",
"clock_out": "2026-09-01T23:30:00.000Z",
"total_hours": 9.5,
"work_date_local": "2026-09-01",
"worker_timezone": "America/Phoenix",
"pay_type": "Overtime",
"is_open": false,
"auto_closed": false,
"approval_status": "pending",
"notes": null,
"edit_history": [
{
"edited_at": "2026-09-01T22:15:00.000Z",
"edited_by": "managerUid",
"edited_by_name": "Ops Manager",
"previous_clock_in": "2026-09-01T14:00:00.000Z",
"previous_clock_out": "2026-09-01T22:00:00.000Z",
"previous_total_hours": 8
}
],
"created_by": "defAuthUidOrPayrollId",
"created_by_name": "Sam Driver",
"updated_by": "managerUid",
"updated_by_name": "Ops Manager",
"updated_at": "2026-09-01T22:15:00.000Z",
"created_at": "2026-09-01T14:00:00.000Z"
}
]
}
}
Users
Tenant user profiles. Responses return the full Firestore user document; samples show the fields most useful for integrations.
List users
GET /tenants/{tenantId}/users
Sample request
curl -sS -X GET \ "https://api.salesgoat.app/api/v1/tenants/YOUR_TENANT_ID/users" \ -H "Authorization: Bearer YOUR_ID_TOKEN"
Sample response
{
"success": true,
"message": "Users retrieved successfully",
"data": [
{
"id": "abcAuthUidOrPayrollId",
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Tech",
"full_name": "Jane Tech",
"office": "Raleigh",
"team_ids": ["team_raleigh_id"],
"team_names": ["Raleigh"],
"roles": ["technician"],
"firebase_auth_uid": "abcAuthUidOrPayrollId",
"sales_professional_uid": "000123",
"is_active": true,
"api_user": false
},
{
"id": "defAuthUidOrPayrollId",
"email": "sam@example.com",
"first_name": "Sam",
"last_name": "Driver",
"full_name": "Sam Driver",
"office": "Arizona",
"team_ids": ["team_arizona_id"],
"team_names": ["Arizona"],
"roles": ["rep"],
"firebase_auth_uid": "defAuthUidOrPayrollId",
"sales_professional_uid": "000124",
"is_active": true,
"api_user": false
}
]
}
Get user
GET /tenants/{tenantId}/users/{userId}
Sample request
curl -sS -X GET \ "https://api.salesgoat.app/api/v1/tenants/YOUR_TENANT_ID/users/abcAuthUidOrPayrollId" \ -H "Authorization: Bearer YOUR_ID_TOKEN"
Sample response
{
"success": true,
"message": "User retrieved successfully",
"data": {
"id": "abcAuthUidOrPayrollId",
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Tech",
"full_name": "Jane Tech",
"office": "Raleigh",
"team_ids": ["team_raleigh_id"],
"team_names": ["Raleigh"],
"roles": ["technician"],
"firebase_auth_uid": "abcAuthUidOrPayrollId",
"sales_professional_uid": "000123",
"is_active": true,
"api_user": false
}
}