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

StatusMeaning
400Missing/invalid dates, range too large, bad query
401Missing or invalid credentials / Bearer token
403User not in tenant (or endpoint not allowed)
404Resource not found
500Server error

Pay periods

Pay period windows used with paystub overview totals. No SCPF / line-item detail in v1.

GET /tenants/{tenantId}/pay_periods

ParamRequiredNotes
limitNoDefault 50, max 100
offsetNoDefault 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

ParamRequiredNotes
start / endNoFilter on pay_date (YYYY-MM-DD); provide both or neither
pay_period_idNoExact pay period id
salesProfessionalIdNoSingle rep
limitNoDefault 50, max 500
offsetNoDefault 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

ParamRequiredNotes
salesProfessionalIdNoFilter by rep
projectStatusNoExact status string
limitNoDefault 50
offsetNoDefault 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

ParamRequiredNotes
start / endYesInclusive local work dates; max 31 days
limitNoDefault 500, max 1000
offsetNoDefault 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: office is the worker profile string (e.g. Raleigh). team_ids / team_names are 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_status is pending | approved | rejected (same as the dashboard).
  • Manual edits: edit_history is an append-only list of dashboard/manual clock adjustments. Each item stores the previous clock-in/out/hours plus edited_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 on updated_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
  }
}